-
-
Notifications
You must be signed in to change notification settings - Fork 128
Allow custom FPS values for each GameSpeed index #2092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| ``` | ||
|
|
||
| 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. | ||
|
||
| - 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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
|
|
||
| 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); | ||
|
||
|
|
||
| // 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; | ||
| } | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| value = (int)((double)value / (std::max((double)FPSCounter::CurrentFrameRate, 1.0) / 15.0)); | ||
| return 0; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Phobos-developers/PhobosSupplementaries#14