diff --git a/CREDITS.md b/CREDITS.md index ecf82be902..23eaba7f0c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -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 diff --git a/docs/Miscellanous.md b/docs/Miscellanous.md index f953b34bb8..70f6593882 100644 --- a/docs/Miscellanous.md +++ b/docs/Miscellanous.md @@ -95,26 +95,9 @@ 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 @@ -122,68 +105,26 @@ In `RA2MD.INI`: 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 -
- -
+### 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. +- When used, **game speeds are unified across all game modes** - skirmish, campaign, and multiplayer all use the same FPS values for each speed position. +- Per-speed keys (`CustomGameSpeedFPS.N`) set individual positions. +- Practical maximum is ~1000 FPS (limited by `timeGetTime()` resolution). -
-

-
-
+In `rulesmd.ini`: +```ini +[General] +EnableCustomFPS=false ; boolean (default: false) +CustomGameSpeedFPS.0=0 ; integer, GameSpeed 0 target FPS (default: 0 = vanilla 60 FPS) +CustomGameSpeedFPS.1=0 ; integer, GameSpeed 1 target FPS (default: 0 = vanilla 45 FPS) +CustomGameSpeedFPS.2=0 ; integer, GameSpeed 2 target FPS (default: 0 = vanilla 30 FPS) +CustomGameSpeedFPS.3=0 ; integer, GameSpeed 3 target FPS (default: 0 = vanilla 20 FPS) +CustomGameSpeedFPS.4=0 ; integer, GameSpeed 4 target FPS (default: 0 = vanilla 15 FPS) +CustomGameSpeedFPS.5=0 ; integer, GameSpeed 5 target FPS (default: 0 = vanilla 12 FPS) +CustomGameSpeedFPS.6=0 ; integer, GameSpeed 6 target FPS (default: 0 = vanilla 10 FPS) ``` - ## INI diff --git a/docs/User-Interface.md b/docs/User-Interface.md index 1801e5c9b8..bd1329caaa 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -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. - This behavior is designed to be toggleable by users. For now you can only do that externally via client or manually. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 975aeed572..082ef34c03 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -32,6 +32,7 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] #### From post-0.3 devbuilds +- `CustomGS` and related keys (`CustomGSN.ChangeInterval`, `CustomGSN.ChangeDelay`, `CustomGSN.DefaultDelay`) have been deprecated and replaced by the new direct FPS control system. Use `EnableCustomFPS=true` and `CustomGameSpeedFPS.N` keys instead to set target FPS for each game speed position. The new system works across skirmish, campaign, and multiplayer modes. See [Custom game speed](Miscellanous.md#custom-game-speed) for details. - Ivan bombs no longer automatically center on building when attached. Set `[CombatDamage] -> IvanBombAttachToCenter` to true to restore this behaviour. Due to technical constraints this cannot be customized per WeaponType. - `AlternateFLH` no longer affects vehicle passengers by default. To re-enable it, set `AlternateFLH.ApplyVehicle=true` on the transport unit. - Parsing priority of `ShowBriefing` and `BriefingTheme` between map file and `missionmd.ini` has been switched (from latter taking priority over former to vice-versa) due to technical limitations and compatibility issues with spawner DLL. @@ -535,6 +536,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) diff --git a/src/Misc/Hooks.Gamespeed.cpp b/src/Misc/Hooks.Gamespeed.cpp index 9a64c6afc8..c88f0889c5 100644 --- a/src/Misc/Hooks.Gamespeed.cpp +++ b/src/Misc/Hooks.Gamespeed.cpp @@ -1,97 +1,115 @@ #include -#include +#include +#include #include #include -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); -DEFINE_HOOK(0x55E33B, SyncDelay_End, 0x6) -{ - if (Phobos::Misc::CustomGS && SessionClass::IsSingleplayer()) - FrameTimer.TimeLeft = GameOptionsClass::Instance.GameSpeed; - return 0; + return 0x647C6C; } -// note: currently vanilla code, doesn't do anything, changing PrecalcDesiredFrameRate doesn't effect anything either -/* -void SetNetworkFrameRate() +// Hook MainLoop skirmish/campaign FPS calculation +// The normal route FrameTimer.TimeLeft rounds to 0 for >60 FPS (tick-based timeGetTime >> 4), +// NetworkFrameTimer (ms-based timeGetTime) provides the frame timing that SyncDelay's NetworkFrameTimer loop uses. +// We need to set up NetworkFrameTimer like multiplayer mode does for >60 FPS support. +DEFINE_HOOK(0x55D7B6, MainLoop_SkirmishFPSFix, 0xC) { - DEFINE_REFERENCE(int, PrecalcDesiredFrameRate, 0xA8B550u) - switch (GameOptionsClass::Instance.GameSpeed) + 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 + Unsorted::GameFrameTimer.CurrentTime = timerValue; + Unsorted::GameFrameTimer.TimeLeft = 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 + Unsorted::GameFrameTimer.CurrentTime = timerValue; + Unsorted::GameFrameTimer.TimeLeft = targetFrameDelayTicks; + + // SyncDelay compares elapsed time since CurrentTime against TimeLeft. + Unsorted::NetworkFrameTimer.StartTime = currentTime; + Unsorted::NetworkFrameTimer.CurrentTime = currentTime; + Unsorted::NetworkFrameTimer.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 NetworkFrameTimer path +DEFINE_HOOK(0x55E1B6, SyncDelay_RedirectSkirmishToNetworkFrameTimer, 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 NetworkFrameTimer path like multiplayer + + return 0x55E2B4; // Vanilla FrameTimer path + } + + return 0x55E1BC; } -*/ diff --git a/src/Misc/Hooks.Timers.cpp b/src/Misc/Hooks.Timers.cpp index df650e5b11..2436c31396 100644 --- a/src/Misc/Hooks.Timers.cpp +++ b/src/Misc/Hooks.Timers.cpp @@ -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)) { value = (int)((double)value / (std::max((double)FPSCounter::CurrentFrameRate, 1.0) / 15.0)); return 0; diff --git a/src/Misc/PhobosToolTip.cpp b/src/Misc/PhobosToolTip.cpp index aa30b9692c..ce4d031d37 100644 --- a/src/Misc/PhobosToolTip.cpp +++ b/src/Misc/PhobosToolTip.cpp @@ -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)) { return tickTime / std::max((int)FPSCounter::CurrentFrameRate, 1); } diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index c9bc501bdc..6e584215f0 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -77,10 +77,8 @@ bool Phobos::Config::ShowFlashOnSelecting = false; bool Phobos::Config::UnitPowerDrain = false; int Phobos::Config::SuperWeaponSidebar_RequiredSignificance = 0; -bool Phobos::Misc::CustomGS = false; -int Phobos::Misc::CustomGS_ChangeInterval[7] = { -1, -1, -1, -1, -1, -1, -1 }; -int Phobos::Misc::CustomGS_ChangeDelay[7] = { 0, 1, 2, 3, 4, 5, 6 }; -int Phobos::Misc::CustomGS_DefaultDelay[7] = { 0, 1, 2, 3, 4, 5, 6 }; +bool Phobos::Misc::EnableCustomFPS = false; +int Phobos::Misc::CustomGameSpeedFPS[7] = { 0, 0, 0, 0, 0, 0, 0 }; DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) { @@ -250,26 +248,16 @@ DEFINE_HOOK(0x52D21F, InitRules_ThingsThatShouldntBeSerailized, 0x6) Phobos::Config::ArtImageSwap = pINI_RULESMD->ReadBool(GameStrings::General, "ArtImageSwap", false); Phobos::Config::UnitPowerDrain = pINI_RULESMD->ReadBool(GameStrings::General, "UnitPowerDrain", false); - Phobos::Misc::CustomGS = pINI_RULESMD->ReadBool(GameStrings::General, "CustomGS", false); + // Custom FPS settings + Phobos::Misc::EnableCustomFPS = pINI_RULESMD->ReadBool(GameStrings::General, "EnableCustomFPS", false); - char tempBuffer[26]; - for (size_t i = 0; i <= 6; ++i) + char tempBuffer[32]; + for (int i = 0; i < 7; ++i) { - int temp; - _snprintf_s(tempBuffer, sizeof(tempBuffer), "CustomGS%d.ChangeDelay", 6 - i); - temp = pINI_RULESMD->ReadInteger(GameStrings::General, tempBuffer, -1); - if (temp >= 0 && temp <= 6) - Phobos::Misc::CustomGS_ChangeDelay[i] = 6 - temp; - - _snprintf_s(tempBuffer, sizeof(tempBuffer), "CustomGS%d.DefaultDelay", 6 - i); - temp = pINI_RULESMD->ReadInteger(GameStrings::General, tempBuffer, -1); - if (temp >= 1) - Phobos::Misc::CustomGS_DefaultDelay[i] = 6 - temp; - - _snprintf_s(tempBuffer, sizeof(tempBuffer), "CustomGS%d.ChangeInterval", 6 - i); - temp = pINI_RULESMD->ReadInteger(GameStrings::General, tempBuffer, -1); - if (temp >= 1) - Phobos::Misc::CustomGS_ChangeInterval[i] = temp; + _snprintf_s(tempBuffer, sizeof(tempBuffer), "CustomGameSpeedFPS.%d", i); + Phobos::Misc::CustomGameSpeedFPS[i] = pINI_RULESMD->ReadInteger(GameStrings::General, tempBuffer, Phobos::Misc::CustomGameSpeedFPS[i]); + if (Phobos::Misc::CustomGameSpeedFPS[i] < 0) + Phobos::Misc::CustomGameSpeedFPS[i] = 0; } if (pINI_RULESMD->ReadBool(GameStrings::General, "FixTransparencyBlitters", true)) diff --git a/src/Phobos.h b/src/Phobos.h index 2e794bd388..4fdbcb4cb0 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -117,10 +117,8 @@ class Phobos class Misc { public: - static bool CustomGS; - static int CustomGS_ChangeInterval[7]; - static int CustomGS_ChangeDelay[7]; - static int CustomGS_DefaultDelay[7]; + static int CustomGameSpeedFPS[7]; + static bool EnableCustomFPS; }; class Optimizations