Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,4 +736,6 @@ This page lists all the individual contributions to the project by their author.
- **Damfoos** - extensive and thorough testing
- **Dmitry Volkov** - extensive and thorough testing
- **Rise of the East community** - extensive playtesting of in-dev features
- **11EJDE11** - Prevent mpdebug number from being drawn when visibility toggled off
- **11EJDE11**:
- Prevent mpdebug number from being drawn when visibility toggled off
- Allow customising FPS values
93 changes: 17 additions & 76 deletions docs/Miscellanous.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,95 +95,36 @@ InsigniaType.PassengersN= ; InsigniaType

## Game Speed

### Single player game speed
### Campaign default game speed

- It is now possible to change the default (GS4/Fast/30FPS) campaign game speed with `CampaignDefaultGameSpeed`.
- It is now possible to change the *values* of single player game speed, by inputing a pair of values. This feature must be enabled with `CustomGS=true`. **Only values between 10 and 60 FPS can be consistently achieved.**
- Custom game speed is achieved by periodically manipulating the delay between game frames, thus increasing or decreasing FPS.
- `CustomGSN.ChangeInterval` describes the frame interval between applying the effect. A value of 2 means "every other frame", 3 means "every 3 frames" etc. Increase of speedup/slowdown is approximately logarithmic.
- `CustomGSN.ChangeDelay` sets the delay (game speed number) to use every `CustomGSN.ChangeInterval` frames.
- `CustomGSN.DefaultDelay` sets the delay (game speed number) to use on other frames.
- Using game speed 6 (Fastest) in either `CustomGSN.ChangeDelay` or `CustomGSN.DefaultDelay` allows to set FPS above 60.
- **However, the resulting FPS may vary on different machines.**

In `rulesmd.ini`:
```ini
[General]
CustomGS=false ; boolean
CustomGSN.ChangeInterval=-1 ; integer >= 1
CustomGSN.ChangeDelay=N ; integer between 0 and 6
CustomGSN.DefaultDelay=N ; integer between 0 and 6
; where N = 0, 1, 2, 3, 4, 5, 6
```
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add migration entries to docs and the migration script (see FIXME entries)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b1e580f

Sorry, not seeing any FIXME?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ctrl+F in migration script (see Phobos Supplementaries repo)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


In `RA2MD.INI`:
```ini
[Phobos]
CampaignDefaultGameSpeed=4 ; integer
```

```{note}
Currently there is no way to set desired FPS directly. Use the generator below to get required values. The generator supports values from 10 to 60.
```

```{dropdown} Click to show the generator
Enter desired FPS
<div>
<input id="customGameSpeedIn" type=number oninput="onInput()" style="width:100%";>
</div>
### Custom game speed

Results (remember to replace N with your game speed number!):
- Each of the 7 game speed slider positions (GameSpeed 0-6) can have a custom target FPS set independently. A value of `0` keeps that position at its vanilla FPS.
- Works in skirmish, campaign, and multiplayer.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should spell out explicitly that they will be unified now, so that the modders must disable the different game speed scales in client now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- Per-speed keys (`CustomGameSpeedFPS.N`) set individual positions.
- Practical maximum is ~1000 FPS (limited by `timeGetTime()` resolution).

<div>
</p>
<div id="codeBlockHere1"></div>
</div>
In `rulesmd.ini`:
```ini
[General]
EnableCustomFPS=true ; boolean
CustomGameSpeedFPS.0=140 ; integer, GameSpeed 0 target FPS
CustomGameSpeedFPS.1=120 ; integer, GameSpeed 1 target FPS
CustomGameSpeedFPS.2=100 ; integer, GameSpeed 2 target FPS
CustomGameSpeedFPS.3=90 ; integer, GameSpeed 3 target FPS
CustomGameSpeedFPS.4=80 ; integer, GameSpeed 4 target FPS
CustomGameSpeedFPS.5=70 ; integer, GameSpeed 5 target FPS
CustomGameSpeedFPS.6=0 ; integer, GameSpeed 6 target FPS (0 = default)
```

<script>
makeINICodeBlock(document.getElementById("codeBlockHere1"), "customGameSpeedOut", 400);
let fpsArray = [];
for (let d = 0; d <= 5; d++) {
for (let c = 0; c <= 5; c++) {
for (let i = 1; i <= 40; i++) {
fpsArray.push(Math.round(formula(c, d, i)));
}
}
}
function formula(c, d, i) {
return (60/(6-c)+60/(6-d)*((i-1)/(6-c)))/(1+(i-1)/(6-c));
}
function onInput() {
let fps = document.getElementById("customGameSpeedIn");
let out = document.getElementById("customGameSpeedOut");
out.textContent = ''; // remove all children
out.appendChild(document.createElement("span"));
let j = 0;
let foundAny = false;
while (true) {
j = fpsArray.indexOf(parseInt(fps.value), j);
if (j == -1) {
break;
}
d = Math.floor(j / 240);
c = Math.floor(j % 240 / 40);
i = j % 40 + 1;
j += 1;
let content = [];
if (foundAny) {
content.push({key: null, value: null, comment: "// -- Or -- "});
}
content.push({key: "CustomGSN.DefaultDelay", value: d, comment: null});
content.push({key: "CustomGSN.ChangeDelay", value: c, comment: null});
content.push({key: "CustomGSN.ChangeInterval", value: i, comment: null});
content.forEach(line => addINILine(out, line));
foundAny = true;
}
if (!foundAny) {
addINILine(out, {key: null, value: null, comment: "// Sorry, couldn't find anything! 本工具无能为力"});
}
}
</script>

## INI

Expand Down
5 changes: 2 additions & 3 deletions docs/User-Interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,8 @@ ShowPlacementPreview=yes ; boolean
### Real time timers

- Timers can now display values in real time, taking game speed into account. This can be enabled with `RealTimeTimers=true`.
- By default, time is calculated relative to desired framerate. Enabling `RealTimeTimers.Adaptive` (always true for unlimited FPS and custom speeds) will calculate time relative to *current* FPS, accounting for lag.
- When playing with unlimited FPS (or custom speed above 60 FPS), the timers might constantly change value because of the unstable nature.
- This option respects custom game speeds.
- By default, time is calculated relative to the desired framerate for the current GameSpeed. Enabling `RealTimeTimers.Adaptive` (always forced on when GameSpeed is 0/Fastest or a custom game speed is active) will calculate time relative to *current* FPS, accounting for lag.
- When playing with a custom FPS above 60, the timers might constantly fluctuate due to frame-to-frame variance.
Comment on lines +291 to +292
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should amend this according to the comments


- This behavior is designed to be toggleable by users. For now you can only do that externally via client or manually.

Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ New:
- Option to scale `PowerSurplus` setting if enabled to current power drain with `PowerSurplus.ScaleToDrainAmount` (by Starkku)
- Global default value for `DefaultToGuardArea` (by TaranDahl)
- [Weapon range finding in cylinder](New-or-Enhanced-Logics.md#range-finding-in-cylinder) (by TaranDahl)
- [Enhanced custom game speed with direct FPS control and multiplayer support](Miscellanous.md#custom-game-speed) (by 11EJDE11)

Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
Expand Down
168 changes: 101 additions & 67 deletions src/Misc/Hooks.Gamespeed.cpp
Original file line number Diff line number Diff line change
@@ -1,97 +1,131 @@
#include <Phobos.h>
#include <Helpers/Macro.h>
#include <Utilities/Macro.h>
#include <algorithm>
#include <SessionClass.h>
#include <GameOptionsClass.h>

namespace GameSpeedTemp
{
static int counter = 0;
}

DEFINE_HOOK(0x69BAE7, SessionClass_Resume_CampaignGameSpeed, 0xA)
{
GameOptionsClass::Instance.GameSpeed = Phobos::Config::CampaignDefaultGameSpeed;
return 0x69BAF1;
}

DEFINE_REFERENCE(CDTimerClass, FrameTimer, 0x887348)
// For custom game speeds:
// Add to rulesmd.ini under [General]:
// EnableCustomFPS=yes ; Enable/disable custom FPS
// CustomGameSpeedFPS.0=120 ; Per-speed FPS. 0 = vanilla. Vanilla: 0=60, 1=45, 2=30, 3=20, 4=15, 5=12, 6=10
//
// -Each speed slot with a non-zero CustomGameSpeedFPS.N runs at that target FPS
// -Slots with 0 (or unset) use vanilla FPS for that position
// -Practical max is ~1000 FPS (timeGetTime() resolution)

// Queue_AI_Multiplayer
// Patch v26 to INT_MAX disables the 60fps cap on multiplayer.
DEFINE_PATCH(0x647C28, 0xBE, 0xFF, 0xFF, 0xFF, 0x7F); // mov esi, INT_MAX

DEFINE_HOOK(0x55E160, SyncDelay_Start, 0x6)
// Queue_AI_Multiplayer
// Override the GameSpeed-to-fps calculation in multiplayer.
DEFINE_HOOK(0x647C4D, Queue_AI_Multiplayer_CustomFPSCalculation, 0x1F)
{
//DEFINE_NONSTATIC_REFERENCE(CDTimerClass, NFTTimer, 0x887328);
if (!Phobos::Misc::CustomGS || SessionClass::IsMultiplayer())
return 0;
if ((Phobos::Misc::CustomGS_ChangeInterval[FrameTimer.TimeLeft] > 0)
&& (GameSpeedTemp::counter % Phobos::Misc::CustomGS_ChangeInterval[FrameTimer.TimeLeft] == 0))
int gameSpeed = GameOptionsClass::Instance.GameSpeed;
int calculatedFPS;

if (Phobos::Misc::EnableCustomFPS && Phobos::Misc::CustomGameSpeedFPS[gameSpeed] > 0)
{
calculatedFPS = Phobos::Misc::CustomGameSpeedFPS[gameSpeed];
}
else if (gameSpeed == 0)
{
FrameTimer.TimeLeft = Phobos::Misc::CustomGS_ChangeDelay[FrameTimer.TimeLeft];
GameSpeedTemp::counter = 1;
// Vanilla: GameSpeed 0 = 60 FPS
calculatedFPS = 60;
}
else if (gameSpeed == 1)
{
// Vanilla: GameSpeed 1 = 45 FPS
calculatedFPS = 45;
}
else
{
FrameTimer.TimeLeft = Phobos::Misc::CustomGS_DefaultDelay[FrameTimer.TimeLeft];
GameSpeedTemp::counter++;
// Vanilla: GameSpeed 2+ = 60 / GameSpeed
calculatedFPS = 60 / gameSpeed;
}

return 0;
R->EAX(calculatedFPS);

return 0x647C6C;
}

DEFINE_HOOK(0x55E33B, SyncDelay_End, 0x6)
struct NFTTimerStruct
{
if (Phobos::Misc::CustomGS && SessionClass::IsSingleplayer())
FrameTimer.TimeLeft = GameOptionsClass::Instance.GameSpeed;
return 0;
}
DWORD StartTime;
DWORD CurrentTime;
int TimeLeft;
};
DEFINE_REFERENCE(NFTTimerStruct, NFTTimer, 0x887328);

// note: currently vanilla code, doesn't do anything, changing PrecalcDesiredFrameRate doesn't effect anything either
/*
void SetNetworkFrameRate()
struct FrameTimerStruct
{
DEFINE_REFERENCE(int, PrecalcDesiredFrameRate, 0xA8B550u)
switch (GameOptionsClass::Instance.GameSpeed)
DWORD StartTime;
DWORD CurrentTime;
int DelayTime;
};
DEFINE_REFERENCE(FrameTimerStruct, GameFrameTimer, 0x887348);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those may already exist in YRpp? There's some confusion with them there, maybe @ZivDero could help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing them there. There's Timer.h but that looks more like something you'd create rather than use when accessing existing ones?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those may already exist in YRpp? There's some confusion with them there, maybe @ZivDero could help.

Well the structs at least exist at
https://github.com/Phobos-developers/YRpp/blob/7a43c8bc7872d81c27ce470a4f0bf4d1c3a8e224/Timer.h

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addresses don't seem to show up in YRpp so they're probably not in; Should be added there instead.
Their names are FrameTimer (from RA) and the other one we called NetworkFrameTimer.

CDTimerClass<SystemTimerClass> FrameTimer;

CDTimerClass<MillisecondSystemTimerClass> NetworkFrameTimer;

CDTimerClass is what YRpp calls TimerStruct. SystemTimerClass is just SystemTimer.
MillisecondSystemTimerClass however is not present in YRpp it seems, and it is not the same as SystemTimerClass or MillisecondTimerClass.

I would provide the address but IDA isn't cooperating...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting at 5D5870, ctor, dtor, operator(), operator long

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Ziv. Can you please check Phobos-developers/YRpp#59


// Hook MainLoop skirmish/campaign FPS calculation
// The normal route FrameTimer.DelayTime rounds to 0 for >60 FPS (tick-based timeGetTime >> 4),
// NFTTimer (ms-based timeGetTime) provides the frame timing that SyncDelay's NFTTimer loop uses.
// We need to set up NFTTimer like multiplayer mode does for >60 FPS support.
DEFINE_HOOK(0x55D7B6, MainLoop_SkirmishFPSFix, 0xC)
{
const DWORD timerValue = R->ECX();
const int gameSpeed = R->ESI();

const bool shouldUseCustomFPS = Phobos::Misc::EnableCustomFPS
&& Phobos::Misc::CustomGameSpeedFPS[gameSpeed] > 0
&& (SessionClass::IsSkirmish() || SessionClass::IsCampaign());

if (!shouldUseCustomFPS)
{
case 0:
Game::Network.MaxAhead = 40;
PrecalcDesiredFrameRate = 60;
Game::Network.FrameSendRate = 10;
break;
case 1:
Game::Network.MaxAhead = 40;
PrecalcDesiredFrameRate = 45;
Game::Network.FrameSendRate = 10;
break;
case 2:
Game::Network.MaxAhead = 30;
PrecalcDesiredFrameRate = 30;
break;
case 3:
Game::Network.MaxAhead = 20;
PrecalcDesiredFrameRate = 20;
break;
case 4:
Game::Network.MaxAhead = 20;
PrecalcDesiredFrameRate = 15;
break;
case 5:
Game::Network.MaxAhead = 20;
PrecalcDesiredFrameRate = 12;
break;
default:
Game::Network.MaxAhead = 10;
PrecalcDesiredFrameRate = 10;
break;
// Use vanilla behavior
GameFrameTimer.CurrentTime = timerValue;
GameFrameTimer.DelayTime = gameSpeed;
return 0x55D7C2;
}
}

DEFINE_HOOK(0x5C49A7, MPCooperative_5C46E0_FPS, 0x9)
{
SetNetworkFrameRate();
return 0x5C4A40;
const int customFPS = Phobos::Misc::CustomGameSpeedFPS[gameSpeed];

// calc frame timings
const int targetFrameDelayTicks = 60 / customFPS;
const int targetFrameTimeMs = std::max(1, 1000 / customFPS); // 1ms floor = ~1000 FPS ceiling

const DWORD currentTime = timeGetTime();

// just in case
GameFrameTimer.CurrentTime = timerValue;
GameFrameTimer.DelayTime = targetFrameDelayTicks;

// SyncDelay compares elapsed time since CurrentTime against TimeLeft.
NFTTimer.StartTime = currentTime;
NFTTimer.CurrentTime = currentTime;
NFTTimer.TimeLeft = targetFrameTimeMs;

// "Requested FPS"
SessionClass::Instance.DesiredFrameRate = customFPS;

return 0x55D7C2; // Past the two MOVs we replaced
}

DEFINE_HOOK(0x794F7E, Start_Game_Now_FPS, 0x8)
// SyncDelay
// Redirect skirmish/campaign to the NFTTimer path
DEFINE_HOOK(0x55E1B6, SyncDelay_RedirectSkirmishToNFTTimer, 0x6)
{
SetNetworkFrameRate();
return 0x79501F;
if (SessionClass::IsSkirmish() || SessionClass::IsCampaign())
{
if (Phobos::Misc::EnableCustomFPS && Phobos::Misc::CustomGameSpeedFPS[GameOptionsClass::Instance.GameSpeed] > 0)
return 0x55E1BC; // Custom FPS: use NFTTimer path like multiplayer

return 0x55E2B4; // Vanilla FrameTimer path
}

return 0x55E1BC;
}
*/
2 changes: 1 addition & 1 deletion src/Misc/Hooks.Timers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ DEFINE_HOOK(0x6D4B50, PrintTimerOnTactical_Start, 0x6)

if (Phobos::Config::RealTimeTimers_Adaptive
|| GameOptionsClass::Instance.GameSpeed == 0
|| (Phobos::Misc::CustomGS && !SessionClass::IsMultiplayer()))
|| (Phobos::Misc::EnableCustomFPS && Phobos::Misc::CustomGameSpeedFPS[GameOptionsClass::Instance.GameSpeed] > 0))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think you amending this check like this is actually making it worse. Since we have target FPS for all cases if custom game speed is on, we should actually bail from this branch if we have a custom game speed without adaptive timers set. So the check should read like "if adaptive OR vanilla FPS values AND game speed == 0".

{
value = (int)((double)value / (std::max((double)FPSCounter::CurrentFrameRate, 1.0) / 15.0));
return 0;
Expand Down
2 changes: 1 addition & 1 deletion src/Misc/PhobosToolTip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ inline static int TickTimeToSeconds(int tickTime)

if (Phobos::Config::RealTimeTimers_Adaptive
|| GameOptionsClass::Instance.GameSpeed == 0
|| (Phobos::Misc::CustomGS && !SessionClass::IsMultiplayer()))
|| (Phobos::Misc::EnableCustomFPS && Phobos::Misc::CustomGameSpeedFPS[GameOptionsClass::Instance.GameSpeed] > 0))
Comment on lines 110 to +112
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should unify with the one I commented on as a common display value calc function (and any other instances if present)

{
return tickTime / std::max((int)FPSCounter::CurrentFrameRate, 1);
}
Expand Down
Loading