diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index 494d544f73..ceb8762557 100644
--- a/Core/GameEngine/CMakeLists.txt
+++ b/Core/GameEngine/CMakeLists.txt
@@ -39,6 +39,7 @@ set(GAMEENGINE_SRC
# Include/Common/Errors.h
Include/Common/file.h
Include/Common/FileSystem.h
+ Include/Common/FrameRateLimit.h
# Include/Common/FunctionLexicon.h
Include/Common/GameAudio.h
# Include/Common/GameCommon.h
@@ -569,6 +570,7 @@ set(GAMEENGINE_SRC
# Source/Common/DamageFX.cpp
# Source/Common/Dict.cpp
# Source/Common/DiscreteCircle.cpp
+ Source/Common/FrameRateLimit.cpp
# Source/Common/GameEngine.cpp
# Source/Common/GameLOD.cpp
# Source/Common/GameMain.cpp
diff --git a/Core/GameEngine/Include/Common/FrameRateLimit.h b/Core/GameEngine/Include/Common/FrameRateLimit.h
new file mode 100644
index 0000000000..2ddc9136c0
--- /dev/null
+++ b/Core/GameEngine/Include/Common/FrameRateLimit.h
@@ -0,0 +1,70 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+class FrameRateLimit
+{
+public:
+ FrameRateLimit();
+
+ Real wait(UnsignedInt maxFps);
+
+private:
+ LARGE_INTEGER m_freq;
+ LARGE_INTEGER m_start;
+};
+
+
+enum FpsValueChange
+{
+ FpsValueChange_Increase,
+ FpsValueChange_Decrease,
+};
+
+
+class RenderFpsPreset
+{
+public:
+ enum CPP_11(: UnsignedInt)
+ {
+ UncappedFpsValue = 1000,
+ };
+
+ static UnsignedInt getNextFpsValue(UnsignedInt value);
+ static UnsignedInt getPrevFpsValue(UnsignedInt value);
+ static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change);
+
+private:
+ static const UnsignedInt s_fpsValues[];
+};
+
+
+class LogicTimeScaleFpsPreset
+{
+public:
+ enum CPP_11(: UnsignedInt)
+ {
+ StepFpsValue = 5,
+ };
+
+ static UnsignedInt getNextFpsValue(UnsignedInt value);
+ static UnsignedInt getPrevFpsValue(UnsignedInt value);
+ static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change);
+};
+
diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp
new file mode 100644
index 0000000000..71fb213d65
--- /dev/null
+++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp
@@ -0,0 +1,126 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#include "PreRTS.h"
+#include "Common/FrameRateLimit.h"
+
+
+FrameRateLimit::FrameRateLimit()
+{
+ QueryPerformanceFrequency(&m_freq);
+ QueryPerformanceCounter(&m_start);
+}
+
+Real FrameRateLimit::wait(UnsignedInt maxFps)
+{
+ LARGE_INTEGER tick;
+ QueryPerformanceCounter(&tick);
+ double elapsedSeconds = static_cast(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
+ const double targetSeconds = 1.0 / maxFps;
+ const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait
+
+ if (sleepSeconds > 0.0)
+ {
+ // Non busy wait with Munkee sleep
+ DWORD dwMilliseconds = static_cast(sleepSeconds * 1000);
+ Sleep(dwMilliseconds);
+ }
+
+ // Busy wait for remaining time
+ do
+ {
+ QueryPerformanceCounter(&tick);
+ elapsedSeconds = static_cast(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
+ }
+ while (elapsedSeconds < targetSeconds);
+
+ m_start = tick;
+ return (Real)elapsedSeconds;
+}
+
+
+const UnsignedInt RenderFpsPreset::s_fpsValues[] = {
+ 30, 50, 56, 60, 65, 70, 72, 75, 80, 85, 90, 100, 110, 120, 144, 240, 480, UncappedFpsValue };
+
+static_assert(LOGICFRAMES_PER_SECOND <= 30, "Min FPS values need to be revisited!");
+
+UnsignedInt RenderFpsPreset::getNextFpsValue(UnsignedInt value)
+{
+ const Int first = 0;
+ const Int last = ARRAY_SIZE(s_fpsValues) - 1;
+ for (Int i = first; i < last; ++i)
+ {
+ if (value >= s_fpsValues[i] && value < s_fpsValues[i + 1])
+ {
+ return s_fpsValues[i + 1];
+ }
+ }
+ return s_fpsValues[last];
+}
+
+UnsignedInt RenderFpsPreset::getPrevFpsValue(UnsignedInt value)
+{
+ const Int first = 0;
+ const Int last = ARRAY_SIZE(s_fpsValues) - 1;
+ for (Int i = last; i > first; --i)
+ {
+ if (value <= s_fpsValues[i] && value > s_fpsValues[i - 1])
+ {
+ return s_fpsValues[i - 1];
+ }
+ }
+ return s_fpsValues[first];
+}
+
+UnsignedInt RenderFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change)
+{
+ switch (change)
+ {
+ default:
+ case FpsValueChange_Increase: return getNextFpsValue(value);
+ case FpsValueChange_Decrease: return getPrevFpsValue(value);
+ }
+}
+
+
+UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value)
+{
+ return value + StepFpsValue;
+}
+
+UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value)
+{
+ if (value - StepFpsValue < LOGICFRAMES_PER_SECOND)
+ {
+ return LOGICFRAMES_PER_SECOND;
+ }
+ else
+ {
+ return value - StepFpsValue;
+ }
+}
+
+UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change)
+{
+ switch (change)
+ {
+ default:
+ case FpsValueChange_Increase: return getNextFpsValue(value);
+ case FpsValueChange_Decrease: return getPrevFpsValue(value);
+ }
+}
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h
index 02e9c39e4e..86f524551e 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h
@@ -34,8 +34,6 @@
#include "Common/SubsystemInterface.h"
#include "Common/GameType.h"
-#define DEFAULT_MAX_FPS 45
-
// forward declarations
class AudioManager;
class GameLogic;
@@ -72,8 +70,20 @@ class GameEngine : public SubsystemInterface
virtual void execute( void ); /**< The "main loop" of the game engine.
It will not return until the game exits. */
- virtual void setFramesPerSecondLimit( Int fps ); ///< Set the maximum rate engine updates are allowed to occur
- virtual Int getFramesPerSecondLimit( void ); ///< Get maxFPS. Not inline since it is called from another lib.
+
+ virtual void setFramesPerSecondLimit( Int fps ); ///< Set the max render and engine update fps.
+ virtual Int getFramesPerSecondLimit( void ); ///< Get the max render and engine update fps.
+ Real getUpdateTime(); ///< Get the last engine update delta time.
+ Real getUpdateFps(); ///< Get the last engine update fps.
+
+ virtual void setLogicTimeScaleFps( Int fps ); ///< Set the logic time scale fps and therefore scale the simulation time. Is capped by the max render fps and does not apply to network matches.
+ virtual Int getLogicTimeScaleFps(); ///< Get the raw logic time scale fps value.
+ virtual void enableLogicTimeScale( Bool enable ); ///< Enable the logic time scale setup. If disabled, the simulation time scale is bound to the render frame time or network update time.
+ virtual Bool isLogicTimeScaleEnabled(); ///< Check whether the logic time scale setup is enabled.
+ Int getActualLogicTimeScaleFps(); ///< Get the real logic time scale fps, depending on the max render fps, network state and enabled state.
+ Real getActualLogicTimeScaleRatio(); ///< Get the real logic time scale ratio, depending on the max render fps, network state and enabled state.
+ Real getActualLogicTimeScaleOverFpsRatio(); ///< Get the real logic time scale over render fps ratio, used to scale down steps in render updates to match logic updates.
+
virtual void setQuitting( Bool quitting ); ///< set quitting status
virtual Bool getQuitting(void); ///< is app getting ready to quit.
@@ -100,9 +110,15 @@ class GameEngine : public SubsystemInterface
virtual ParticleSystemManager* createParticleSystemManager( void ) = 0;
virtual AudioManager *createAudioManager( void ) = 0; ///< Factory for Audio Manager
- Int m_maxFPS; ///< Maximum frames per second allowed
+ Int m_maxFPS; ///< Maximum frames per second for rendering
+ Int m_logicTimeScaleFPS; ///< Maximum frames per second for logic time scale
+
+ Real m_updateTime; ///< Last engine update delta time
+ Real m_logicTimeAccumulator; ///< Frame time accumulated towards submitting a new logic frame
+
Bool m_quitting; ///< true when we need to quit the game
Bool m_isActive; ///< app has OS focus.
+ Bool m_enableLogicTimeScale;
};
inline void GameEngine::setQuitting( Bool quitting ) { m_quitting = quitting; }
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
index e34faf16d2..9bfeee47b3 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
@@ -241,6 +241,10 @@ class GameMessage : public MemoryPoolObject
MSG_META_HELP, ///< bring up help screen
#endif
+ MSG_META_INCREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Increase the max render fps
+ MSG_META_DECREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Decrease the max render fps
+ MSG_META_INCREASE_LOGIC_TIME_SCALE, ///< TheSuperHackers @feature Increase the logic time scale
+ MSG_META_DECREASE_LOGIC_TIME_SCALE, ///< TheSuperHackers @feature Decrease the logic time scale
MSG_META_TOGGLE_LOWER_DETAILS, ///< toggles graphics options to crappy mode instantly
MSG_META_TOGGLE_CONTROL_BAR, ///< show/hide controlbar
diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h
index 3fcc01a078..7b889b9c0b 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h
@@ -109,6 +109,7 @@ class Display : public SubsystemInterface
virtual void drawViews( void ); ///< Render all views of the world
virtual void updateViews ( void ); ///< Updates state of world views
+ virtual void stepViews(); ///< Update views for every fixed time step
virtual VideoBuffer* createVideoBuffer( void ) = 0; ///< Create a video buffer that can be used for this display
@@ -118,6 +119,7 @@ class Display : public SubsystemInterface
virtual Bool isClippingEnabled( void ) = 0;
virtual void enableClipping( Bool onoff ) = 0;
+ virtual void step() {}; ///< Do one fixed time step
virtual void draw( void ); ///< Redraw the entire display
virtual void setTimeOfDay( TimeOfDay tod ) = 0; ///< Set the time of day for this display
virtual void createLightPulse( const Coord3D *pos, const RGBColor *color, Real innerRadius,Real attenuationWidth,
diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
index 72248a9e04..634b78545a 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
@@ -99,6 +99,8 @@ class GameClient : public SubsystemInterface,
virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number
virtual void registerDrawable( Drawable *draw ); ///< Given a drawable, register it with the GameClient and give it a unique ID
+ void step(); ///< Do one fixed time step
+
void updateHeadless();
void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table
diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h b/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h
index 1d1a19dea3..cb358886cc 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h
@@ -69,6 +69,24 @@ static const LookupListRec CategoryListName[] =
// KeyDefType; this is extremely important to maintain!
enum MappableKeyType CPP_11(: Int)
{
+ // keypad keys ----------------------------------------------------------------
+ MK_KP0 = KEY_KP0,
+ MK_KP1 = KEY_KP1,
+ MK_KP2 = KEY_KP2,
+ MK_KP3 = KEY_KP3,
+ MK_KP4 = KEY_KP4,
+ MK_KP5 = KEY_KP5,
+ MK_KP6 = KEY_KP6,
+ MK_KP7 = KEY_KP7,
+ MK_KP8 = KEY_KP8,
+ MK_KP9 = KEY_KP9,
+ MK_KPDEL = KEY_KPDEL,
+ MK_KPSTAR = KEY_KPSTAR,
+ MK_KPMINUS = KEY_KPMINUS,
+ MK_KPPLUS = KEY_KPPLUS,
+ MK_KPENTER = KEY_KPENTER,
+ MK_KPSLASH = KEY_KPSLASH,
+
MK_ESC = KEY_ESC,
MK_BACKSPACE = KEY_BACKSPACE,
MK_ENTER = KEY_ENTER,
@@ -122,16 +140,6 @@ enum MappableKeyType CPP_11(: Int)
MK_8 = KEY_8,
MK_9 = KEY_9,
MK_0 = KEY_0,
- MK_KP1 = KEY_KP1,
- MK_KP2 = KEY_KP2,
- MK_KP3 = KEY_KP3,
- MK_KP4 = KEY_KP4,
- MK_KP5 = KEY_KP5,
- MK_KP6 = KEY_KP6,
- MK_KP7 = KEY_KP7,
- MK_KP8 = KEY_KP8,
- MK_KP9 = KEY_KP9,
- MK_KP0 = KEY_KP0,
MK_MINUS = KEY_MINUS,
MK_EQUAL = KEY_EQUAL,
MK_LBRACKET = KEY_LBRACKET,
@@ -153,13 +161,30 @@ enum MappableKeyType CPP_11(: Int)
MK_PGDN = KEY_PGDN,
MK_INS = KEY_INS,
MK_DEL = KEY_DEL,
- MK_KPSLASH = KEY_KPSLASH,
MK_NONE = KEY_NONE
};
static const LookupListRec KeyNames[] =
{
+ // keypad keys ----------------------------------------------------------------
+ { "KEY_KP0", MK_KP0 },
+ { "KEY_KP1", MK_KP1 },
+ { "KEY_KP2", MK_KP2 },
+ { "KEY_KP3", MK_KP3 },
+ { "KEY_KP4", MK_KP4 },
+ { "KEY_KP5", MK_KP5 },
+ { "KEY_KP6", MK_KP6 },
+ { "KEY_KP7", MK_KP7 },
+ { "KEY_KP8", MK_KP8 },
+ { "KEY_KP9", MK_KP9 },
+ { "KEY_KPDEL", MK_KPDEL },
+ { "KEY_KPSTAR", MK_KPSTAR },
+ { "KEY_KPMINUS", MK_KPMINUS },
+ { "KEY_KPPLUS", MK_KPPLUS },
+ { "KEY_KPENTER", MK_KPENTER },
+ { "KEY_KPSLASH", MK_KPSLASH },
+
{ "KEY_ESC", MK_ESC },
{ "KEY_BACKSPACE", MK_BACKSPACE },
{ "KEY_ENTER", MK_ENTER },
@@ -213,16 +238,6 @@ static const LookupListRec KeyNames[] =
{ "KEY_8", MK_8 },
{ "KEY_9", MK_9 },
{ "KEY_0", MK_0 },
- { "KEY_KP1", MK_KP1 },
- { "KEY_KP2", MK_KP2 },
- { "KEY_KP3", MK_KP3 },
- { "KEY_KP4", MK_KP4 },
- { "KEY_KP5", MK_KP5 },
- { "KEY_KP6", MK_KP6 },
- { "KEY_KP7", MK_KP7 },
- { "KEY_KP8", MK_KP8 },
- { "KEY_KP9", MK_KP9 },
- { "KEY_KP0", MK_KP0 },
{ "KEY_MINUS", MK_MINUS },
{ "KEY_EQUAL", MK_EQUAL },
{ "KEY_LBRACKET", MK_LBRACKET },
@@ -244,7 +259,6 @@ static const LookupListRec KeyNames[] =
{ "KEY_PGDN", MK_PGDN },
{ "KEY_INS", MK_INS },
{ "KEY_DEL", MK_DEL },
- { "KEY_KPSLASH", MK_KPSLASH },
{ "KEY_NONE", MK_NONE },
{ NULL, 0 } // keep this last!
};
@@ -301,7 +315,9 @@ enum CommandUsableInType CPP_11(: Int)
COMMANDUSABLE_NONE = 0,
COMMANDUSABLE_SHELL = (1 << 0),
- COMMANDUSABLE_GAME = (1 << 1)
+ COMMANDUSABLE_GAME = (1 << 1),
+
+ COMMANDUSABLE_EVERYWHERE = COMMANDUSABLE_SHELL | COMMANDUSABLE_GAME,
};
static const char* TheCommandUsableInNames[] =
diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/View.h b/GeneralsMD/Code/GameEngine/Include/GameClient/View.h
index 8afbd829a0..a72a360c4c 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/View.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/View.h
@@ -184,8 +184,7 @@ class View : public Snapshot
virtual void setZoom(Real z) { }
virtual Real getHeightAboveGround() { return m_heightAboveGround; }
virtual void setHeightAboveGround(Real z) { m_heightAboveGround = z; }
- virtual void zoomIn( void ); ///< Zoom in, closer to the ground, limit to min
- virtual void zoomOut( void ); ///< Zoom out, farther away from the ground, limit to max
+ virtual void zoom( Real height ); ///< Zoom in/out, closer to the ground, limit to min, or farther away from the ground, limit to max
virtual void setZoomToDefault( void ) { } ///< Set zoom to default value
virtual Real getMaxZoom( void ) { return m_maxZoom; } ///< return max zoom value
virtual void setOkToAdjustHeight( Bool val ) { m_okToAdjustHeight = val; } ///< Set this to adjust camera height
@@ -211,6 +210,7 @@ class View : public Snapshot
virtual void drawView( void ) = 0; ///< Render the world visible in this view.
virtual void updateView(void) = 0; ///m_useFpsLimit == %d)", fps, TheGlobalData->m_useFpsLimit));
m_maxFPS = fps;
}
+//-------------------------------------------------------------------------------------------------
+Int GameEngine::getFramesPerSecondLimit( void )
+{
+ return m_maxFPS;
+}
+
+//-------------------------------------------------------------------------------------------------
+Real GameEngine::getUpdateTime()
+{
+ return m_updateTime;
+}
+
+//-------------------------------------------------------------------------------------------------
+Real GameEngine::getUpdateFps()
+{
+ return 1.0f / m_updateTime;
+}
+
+//-------------------------------------------------------------------------------------------------
+void GameEngine::setLogicTimeScaleFps( Int fps )
+{
+ m_logicTimeScaleFPS = fps;
+}
+
+//-------------------------------------------------------------------------------------------------
+Int GameEngine::getLogicTimeScaleFps()
+{
+ return m_logicTimeScaleFPS;
+}
+
+//-------------------------------------------------------------------------------------------------
+void GameEngine::enableLogicTimeScale( Bool enable )
+{
+ m_enableLogicTimeScale = enable;
+}
+
+//-------------------------------------------------------------------------------------------------
+Bool GameEngine::isLogicTimeScaleEnabled()
+{
+ return m_enableLogicTimeScale;
+}
+
+//-------------------------------------------------------------------------------------------------
+Int GameEngine::getActualLogicTimeScaleFps( void )
+{
+ if (TheNetwork != NULL)
+ {
+ return TheNetwork->getFrameRate();
+ }
+ else
+ {
+ const Bool enabled = isLogicTimeScaleEnabled();
+ const Int logicTimeScaleFps = getLogicTimeScaleFps();
+ const Int maxFps = getFramesPerSecondLimit();
+
+ if (!enabled || logicTimeScaleFps >= maxFps)
+ {
+ return getFramesPerSecondLimit();
+ }
+ else
+ {
+ return logicTimeScaleFps;
+ }
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+Real GameEngine::getActualLogicTimeScaleRatio()
+{
+ return (Real)getActualLogicTimeScaleFps() / LOGICFRAMES_PER_SECONDS_REAL;
+}
+
+//-------------------------------------------------------------------------------------------------
+Real GameEngine::getActualLogicTimeScaleOverFpsRatio()
+{
+ // TheSuperHackers @info Clamps ratio to min 1, because the logic
+ // frame rate is (typically) capped by the render frame rate.
+ return min(1.0f, (Real)getActualLogicTimeScaleFps() / getUpdateFps());
+}
+
/** -----------------------------------------------------------------------------------------------
* Initialize the game engine by initializing the GameLogic and GameClient.
*/
@@ -358,7 +437,6 @@ void GameEngine::init()
char Buf[256];//////////////////////////////////////////////////////////////////////
#endif//////////////////////////////////////////////////////////////////////////////
- m_maxFPS = DEFAULT_MAX_FPS;
TheSubsystemList = MSGNEW("GameEngineSubsystem") SubsystemInterfaceList;
@@ -824,9 +902,49 @@ void GameEngine::update( void )
TheGameLogic->preUpdate();
- if ((TheNetwork == NULL && !TheGameLogic->isGamePaused()) || (TheNetwork && TheNetwork->isFrameDataReady()))
+ if (TheNetwork != NULL)
{
- TheGameLogic->UPDATE();
+ if (TheNetwork->isFrameDataReady())
+ {
+ TheGameClient->step();
+ TheGameLogic->UPDATE();
+ }
+ }
+ else
+ {
+ if (!TheGameLogic->isGamePaused())
+ {
+ const Bool enabled = isLogicTimeScaleEnabled();
+ const Int logicTimeScaleFps = getLogicTimeScaleFps();
+ const Int maxRenderFps = getFramesPerSecondLimit();
+
+#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
+ Bool useFastMode = TheGlobalData->m_TiVOFastMode;
+#else //always allow this cheat key if we're in a replay game.
+ Bool useFastMode = TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame();
+#endif
+
+ if (useFastMode || !enabled || logicTimeScaleFps >= maxRenderFps)
+ {
+ // Logic time scale is uncapped or larger equal Render FPS. Update straight away.
+ TheGameClient->step();
+ TheGameLogic->UPDATE();
+ }
+ else
+ {
+ // TheSuperHackers @tweak xezon 06/08/2025
+ // The logic time step is now decoupled from the render update.
+ const Real targetFrameTime = 1.0f / logicTimeScaleFps;
+ m_logicTimeAccumulator += min(m_updateTime, targetFrameTime);
+
+ if (m_logicTimeAccumulator >= targetFrameTime)
+ {
+ m_logicTimeAccumulator -= targetFrameTime;
+ TheGameClient->step();
+ TheGameLogic->UPDATE();
+ }
+ }
+ }
}
} // end perfGather
@@ -842,8 +960,8 @@ extern HWND ApplicationHWnd;
*/
void GameEngine::execute( void )
{
+ FrameRateLimit* frameRateLimit = new FrameRateLimit();
- DWORD prevTime = timeGetTime();
#if defined(RTS_DEBUG)
DWORD startTime = timeGetTime() / 1000;
#endif
@@ -913,39 +1031,31 @@ void GameEngine::execute( void )
} // perf
{
-
- if (TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast())
{
+ Bool allowFpsLimit = TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast();
- // I'm disabling this in internal because many people need alt-tab capability. If you happen to be
+ // I'm disabling this in debug because many people need alt-tab capability. If you happen to be
// doing performance tuning, please just change this on your local system. -MDC
#if defined(RTS_DEBUG)
- ::Sleep(1); // give everyone else a tiny time slice.
+ if (allowFpsLimit)
+ ::Sleep(1); // give everyone else a tiny time slice.
#endif
#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
- if ( ! TheGlobalData->m_TiVOFastMode )
- #else //always allow this cheatkey if we're in a replaygame.
- if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()))
+ allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode);
+ #else //always allow this cheat key if we're in a replay game.
+ allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame());
#endif
- {
- // limit the framerate
- DWORD now = timeGetTime();
- DWORD limit = (1000.0f/m_maxFPS)-1;
- while (TheGlobalData->m_useFpsLimit && (now - prevTime) < limit)
- {
- ::Sleep(0);
- now = timeGetTime();
- }
- //Int slept = now - prevTime;
- //DEBUG_LOG(("delayed %d",slept));
-
- prevTime = now;
-
- }
-
- }
+ {
+ // TheSuperHackers @bugfix xezon 05/08/2025 Re-implements the frame rate limiter
+ // with higher resolution counters to cap the frame rate more accurately to the desired limit.
+ allowFpsLimit &= TheGlobalData->m_useFpsLimit;
+ const Int maxFps = allowFpsLimit ? getFramesPerSecondLimit() : INT_MAX;
+ m_updateTime = frameRateLimit->wait(maxFps);
+ }
+
+ }
}
} // perfgather for execute_loop
@@ -961,6 +1071,7 @@ void GameEngine::execute( void )
}
+ delete frameRateLimit;
}
/** -----------------------------------------------------------------------------------------------
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
index 3c1f04476f..d7b5970958 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
@@ -346,6 +346,10 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t)
CASE_LABEL(MSG_META_HELP)
#endif
+ CASE_LABEL(MSG_META_INCREASE_MAX_RENDER_FPS)
+ CASE_LABEL(MSG_META_DECREASE_MAX_RENDER_FPS)
+ CASE_LABEL(MSG_META_INCREASE_LOGIC_TIME_SCALE)
+ CASE_LABEL(MSG_META_DECREASE_LOGIC_TIME_SCALE)
CASE_LABEL(MSG_META_TOGGLE_LOWER_DETAILS)
CASE_LABEL(MSG_META_TOGGLE_CONTROL_BAR)
CASE_LABEL(MSG_META_BEGIN_PATH_BUILD)
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp
index 53de8f7b7b..c319db509d 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp
@@ -131,6 +131,14 @@ void Display::updateViews( void )
}
+void Display::stepViews( void )
+{
+
+ for( View *v = m_viewList; v; v = v->getNextView() )
+ v->stepView();
+
+}
+
/// Redraw the entire display
void Display::draw( void )
{
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp
index 0448be6d79..76228beb56 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp
@@ -773,6 +773,11 @@ void GameClient::update( void )
}
} // end update
+void GameClient::step()
+{
+ TheDisplay->step();
+}
+
void GameClient::updateHeadless()
{
// TheSuperHackers @info helmutbuhler 03/05/2025
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp
index e841924b50..90691380d5 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp
@@ -1883,26 +1883,30 @@ void InGameUI::update( void )
layout->runUpdate();
}
- //Handle keyboard camera rotations
- if( m_cameraRotatingLeft && !m_cameraRotatingRight )
+ if (m_cameraRotatingLeft || m_cameraRotatingRight || m_cameraZoomingIn || m_cameraZoomingOut)
{
- //Keyboard rotate left
- TheTacticalView->setAngle( TheTacticalView->getAngle() - TheGlobalData->m_keyboardCameraRotateSpeed );
- }
- if( m_cameraRotatingRight && !m_cameraRotatingLeft )
- {
- //Keyboard rotate right
- TheTacticalView->setAngle( TheTacticalView->getAngle() + TheGlobalData->m_keyboardCameraRotateSpeed );
- }
- if( m_cameraZoomingIn && !m_cameraZoomingOut )
- {
- //Keyboard zoom in
- TheTacticalView->zoomIn();
- }
- if( m_cameraZoomingOut && !m_cameraZoomingIn )
- {
- //Keyboard zoom out
- TheTacticalView->zoomOut();
+ // TheSuperHackers @tweak The camera rotation and zoom are now decoupled from the render update.
+ const Real fpsRatio = 30.0f / TheGameEngine->getUpdateFps();
+ const Real rotateAngle = TheGlobalData->m_keyboardCameraRotateSpeed * fpsRatio;
+ const Real zoomHeight = 10.0f * fpsRatio;
+
+ if( m_cameraRotatingLeft && !m_cameraRotatingRight )
+ {
+ TheTacticalView->setAngle( TheTacticalView->getAngle() - rotateAngle );
+ }
+ else if( m_cameraRotatingRight && !m_cameraRotatingLeft )
+ {
+ TheTacticalView->setAngle( TheTacticalView->getAngle() + rotateAngle );
+ }
+
+ if( m_cameraZoomingIn && !m_cameraZoomingOut )
+ {
+ TheTacticalView->zoom( -zoomHeight );
+ }
+ else if( m_cameraZoomingOut && !m_cameraZoomingIn )
+ {
+ TheTacticalView->zoom( +zoomHeight );
+ }
}
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
index 23e1f89344..2c38f0b37b 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
@@ -32,6 +32,7 @@
#include "Common/AudioAffect.h"
#include "Common/ActionManager.h"
+#include "Common/FrameRateLimit.h"
#include "Common/GameAudio.h"
#include "Common/GameEngine.h"
#include "Common/GameType.h"
@@ -187,6 +188,91 @@ Bool hasThingsInProduction(PlayerType playerType)
#endif // defined(RTS_DEBUG) || defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
+bool changeMaxRenderFps(FpsValueChange change)
+{
+ UnsignedInt maxRenderFps = TheGameEngine->getFramesPerSecondLimit();
+ maxRenderFps = RenderFpsPreset::changeFpsValue(maxRenderFps, change);
+
+ TheGameEngine->setFramesPerSecondLimit(maxRenderFps);
+ TheWritableGlobalData->m_useFpsLimit = (maxRenderFps != RenderFpsPreset::UncappedFpsValue);
+
+ UnicodeString message;
+
+ if (TheWritableGlobalData->m_useFpsLimit)
+ {
+ message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetMaxRenderFps", L"Max Render FPS is %u", maxRenderFps);
+ }
+ else
+ {
+ message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetUncappedRenderFps", L"Max Render FPS is uncapped");
+ }
+
+ TheInGameUI->messageNoFormat(message);
+
+ return true;
+}
+
+bool changeLogicTimeScale(FpsValueChange change)
+{
+ if (TheNetwork != NULL)
+ return false;
+
+ const UnsignedInt maxRenderFps = TheGameEngine->getFramesPerSecondLimit();
+ UnsignedInt maxRenderRemainder = LogicTimeScaleFpsPreset::StepFpsValue;
+ maxRenderRemainder -= maxRenderFps % LogicTimeScaleFpsPreset::StepFpsValue;
+ maxRenderRemainder %= LogicTimeScaleFpsPreset::StepFpsValue;
+
+ UnsignedInt logicTimeScaleFps = TheGameEngine->getLogicTimeScaleFps();
+ // Set the value to the max render fps value plus a bit when time scale is
+ // disabled. This ensures that the time scale does not re-enable with a
+ // 'surprise' value.
+ if (!TheGameEngine->isLogicTimeScaleEnabled())
+ {
+ logicTimeScaleFps = maxRenderFps + maxRenderRemainder;
+ }
+ // Ceil the value at the max render fps value plus a bit so that the next fps
+ // value decrease would undercut the max render fps at the correct step value.
+ // Example: render fps 72 -> logic value ceiled to 75 -> decreased to 70.
+ logicTimeScaleFps = min(logicTimeScaleFps, maxRenderFps + maxRenderRemainder);
+ logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change);
+
+ // Set value before potentially disabling it.
+ if (TheGameEngine->isLogicTimeScaleEnabled())
+ {
+ TheGameEngine->setLogicTimeScaleFps(logicTimeScaleFps);
+ }
+
+ TheGameEngine->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps);
+
+ // Set value after potentially enabling it.
+ if (TheGameEngine->isLogicTimeScaleEnabled())
+ {
+ TheGameEngine->setLogicTimeScaleFps(logicTimeScaleFps);
+ }
+
+ logicTimeScaleFps = TheGameEngine->getLogicTimeScaleFps();
+ const UnsignedInt actualLogicTimeScaleFps = TheGameEngine->getActualLogicTimeScaleFps();
+ const Real actualLogicTimeScaleRatio = TheGameEngine->getActualLogicTimeScaleRatio();
+
+ UnicodeString message;
+
+ if (TheGameEngine->isLogicTimeScaleEnabled())
+ {
+ message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetLogicTimeScaleFps", L"Logic Time Scale FPS is %u (actual %u, ratio %.02f)",
+ logicTimeScaleFps, actualLogicTimeScaleFps, actualLogicTimeScaleRatio);
+ }
+ else
+ {
+ message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetUncappedLogicTimeScaleFps", L"Logic Time Scale FPS is uncapped (actual %u, ratio %.02f)",
+ actualLogicTimeScaleFps, actualLogicTimeScaleRatio);
+ }
+
+ TheInGameUI->messageNoFormat(message);
+
+ return true;
+}
+
+
static Bool isSystemMessage( const GameMessage *msg );
enum{ DROPPED_MAX_PARTICLE_COUNT = 1000};
@@ -3185,6 +3271,46 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
disp = DESTROY_MESSAGE;
break;
+ //-----------------------------------------------------------------------------------------
+ case GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS:
+ {
+ if (changeMaxRenderFps(FpsValueChange_Increase))
+ {
+ disp = DESTROY_MESSAGE;
+ }
+ break;
+ }
+
+ //-----------------------------------------------------------------------------------------
+ case GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS:
+ {
+ if (changeMaxRenderFps(FpsValueChange_Decrease))
+ {
+ disp = DESTROY_MESSAGE;
+ }
+ break;
+ }
+
+ //-----------------------------------------------------------------------------------------
+ case GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE:
+ {
+ if (changeLogicTimeScale(FpsValueChange_Increase))
+ {
+ disp = DESTROY_MESSAGE;
+ }
+ break;
+ }
+
+ //-----------------------------------------------------------------------------------------
+ case GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE:
+ {
+ if (changeLogicTimeScale(FpsValueChange_Decrease))
+ {
+ disp = DESTROY_MESSAGE;
+ }
+ break;
+ }
+
//-----------------------------------------------------------------------------------------
case GameMessage::MSG_META_TOGGLE_LOWER_DETAILS:
{
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp
index 2dbfdf474d..e11440494f 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp
@@ -29,6 +29,7 @@
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#include "Common/GameType.h"
+#include "Common/GameEngine.h"
#include "Common/MessageStream.h"
#include "Common/Player.h"
#include "Common/PlayerList.h"
@@ -366,15 +367,19 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage
Int spin = msg->getArgument( 1 )->integer;
+ // TheSuperHackers @tweak The camera zoom is now decoupled from the render update.
+ const Real fpsRatio = 30.0f / TheGameEngine->getUpdateFps();
+ const Real zoomHeight = 10.0f * fpsRatio;
+
if (spin > 0)
{
for ( ; spin > 0; spin--)
- TheTacticalView->zoomIn();
+ TheTacticalView->zoom( -zoomHeight );
}
else
{
for ( ;spin < 0; spin++ )
- TheTacticalView->zoomOut();
+ TheTacticalView->zoom( +zoomHeight );
}
}
@@ -408,7 +413,7 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage
// The scaling is based on the current logic rate, this provides a consistent scroll speed at all GameClient FPS
// This also fixes scrolling within replays when fast forwarding due to the uncapped FPS
// When the FPS is in excess of the expected frame rate, the ratio will reduce the offset of the cameras movement
- const Real logicToFpsRatio = TheGlobalData->m_framesPerSecondLimit / TheDisplay->getCurrentFPS();
+ const Real logicToFpsRatio = 30.0f / TheGameEngine->getUpdateFps();
switch (m_scrollType)
{
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
index e8d15503a6..7b854dece5 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
@@ -151,6 +151,10 @@ static const LookupListRec GameMessageMetaTypeNames[] =
{ "PLACE_BEACON", GameMessage::MSG_META_PLACE_BEACON },
{ "DELETE_BEACON", GameMessage::MSG_META_REMOVE_BEACON },
{ "OPTIONS", GameMessage::MSG_META_OPTIONS },
+ { "INCREASE_MAX_RENDER_FPS", GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS },
+ { "DECREASE_MAX_RENDER_FPS", GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS },
+ { "INCREASE_LOGIC_TIME_SCALE", GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE },
+ { "DECREASE_LOGIC_TIME_SCALE", GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE },
{ "TOGGLE_LOWER_DETAILS", GameMessage::MSG_META_TOGGLE_LOWER_DETAILS },
{ "TOGGLE_CONTROL_BAR", GameMessage::MSG_META_TOGGLE_CONTROL_BAR },
{ "BEGIN_PATH_BUILD", GameMessage::MSG_META_BEGIN_PATH_BUILD },
@@ -722,6 +726,50 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
// TheSuperHackers @info A default mapping for MSG_META_SELECT_ALL_AIRCRAFT would be useful for Generals
// but is not recommended, because it will cause key mapping conflicts with original game languages.
+ {
+ // Is useful for Generals and Zero Hour.
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_KPPLUS;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
+ {
+ // Is useful for Generals and Zero Hour.
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_KPMINUS;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
+ {
+ // Is useful for Generals and Zero Hour.
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_KPPLUS;
+ map->m_transition = DOWN;
+ map->m_modState = SHIFT_CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
+ {
+ // Is useful for Generals and Zero Hour.
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_KPMINUS;
+ map->m_transition = DOWN;
+ map->m_modState = SHIFT_CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
{
// Is mostly useful for Generals.
MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY);
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp
index e9ca5dbbf6..58b615b06b 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp
@@ -125,14 +125,9 @@ View *View::prependViewToList( View *list )
return this;
}
-void View::zoomIn( void )
+void View::zoom( Real height )
{
- setHeightAboveGround(getHeightAboveGround() - 10.0f);
-}
-
-void View::zoomOut( void )
-{
- setHeightAboveGround(getHeightAboveGround() + 10.0f);
+ setHeightAboveGround(getHeightAboveGround() + height);
}
/**
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index 18539a53e2..80c07119da 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -81,6 +81,7 @@ class W3DDisplay : public Display
virtual Bool isClippingEnabled( void ) { return m_isClippedEnabled; }
virtual void enableClipping( Bool onoff ) { m_isClippedEnabled = onoff; }
+ virtual void step(); ///< Do one fixed time step
virtual void draw( void ); ///< redraw the entire display
/// @todo Replace these light management routines with a LightManager singleton
@@ -162,6 +163,7 @@ class W3DDisplay : public Display
void calculateTerrainLOD(void); ///< Calculate terrain LOD.
void renderLetterBox(UnsignedInt time); ///< draw letter box border
void updateAverageFPS(void); ///< figure out the average fps over the last 30 frames.
+ static Bool isTimeFrozen();
Byte m_initialized; ///< TRUE when system is initialized
LightClass *m_myLight[LightEnvironmentClass::MAX_LIGHTS]; ///< light hack for now
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h
index 76eaa651d2..b9242dda21 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h
@@ -61,8 +61,8 @@ typedef struct
Real cameraAngle[MAX_WAYPOINTS+2]; // Camera Angle;
Int timeMultiplier[MAX_WAYPOINTS+2]; // Time speedup factor.
Real groundHeight[MAX_WAYPOINTS+1]; // Ground height.
- Int totalTimeMilliseconds; // Num of ms to do this movement.
- Int elapsedTimeMilliseconds; // Time since start.
+ Real totalTimeMilliseconds; // Num of ms to do this movement.
+ Real elapsedTimeMilliseconds; // Time since start.
Real totalDistance; // Total length of paths.
Real curSegDistance; // How far we are along the current seg.
Int shutter;
@@ -140,6 +140,7 @@ class W3DView : public View, public SubsystemInterface
virtual void reset( void );
virtual void drawView( void ); ///< draw this view
virtual void updateView(void); ///isTimeFrozen() && !TheTacticalView->isCameraMovementFinished())
+ return true;
+
+ if (TheScriptEngine->isTimeFrozenDebug())
+ return true;
+
+ if (TheScriptEngine->isTimeFrozenScript())
+ return true;
+
+ if (TheGameLogic->isGamePaused())
+ return true;
+
+ return false;
+}
+
+// TheSuperHackers @tweak xezon 12/08/2025 The WW3D Sync is no longer tied
+// to the render update, but is advanced separately for every fixed time step.
+void W3DDisplay::step()
+{
+ // TheSuperHackers @info This will wrap in 1205 hours at 30 fps logic step.
+ static UnsignedInt syncTime = 0;
+
+ extern HWND ApplicationHWnd;
+ if (ApplicationHWnd && ::IsIconic(ApplicationHWnd)) {
+ return;
+ }
+
+ if (TheGlobalData->m_headless)
+ return;
+
+ Bool freezeTime = isTimeFrozen();
+
+ if (!freezeTime)
+ {
+ syncTime += (UnsignedInt)TheW3DFrameLengthInMsec;
+
+ if (TheScriptEngine->isTimeFast())
+ {
+ return;
+ }
+ }
+
+ WW3D::Sync( syncTime );
+
+ stepViews();
+}
+
//DECLARE_PERF_TIMER(BigAssRenderLoop)
// W3DDisplay::draw ===========================================================
@@ -1690,7 +1739,6 @@ Int W3DDisplay::getLastFrameDrawCalls()
void W3DDisplay::draw( void )
{
//USE_PERF_TIMER(W3DDisplay_draw)
- static UnsignedInt syncTime = 0;
extern HWND ApplicationHWnd;
if (ApplicationHWnd && ::IsIconic(ApplicationHWnd)) {
@@ -1735,10 +1783,6 @@ void W3DDisplay::draw( void )
TheInGameUI->message( UnicodeString( L"-stats is running, at interval: %d." ), TheGlobalData->m_statsInterval );
}
}
-
-
-
-
#endif
// compute debug statistics for display later
@@ -1779,21 +1823,19 @@ void W3DDisplay::draw( void )
//
//PredictiveLODOptimizerClass::Optimize_LODs( 5000 );
- Bool freezeTime = TheTacticalView->isTimeFrozen() && !TheTacticalView->isCameraMovementFinished();
- freezeTime = freezeTime || TheScriptEngine->isTimeFrozenDebug() || TheScriptEngine->isTimeFrozenScript();
- freezeTime = freezeTime || TheGameLogic->isGamePaused();
+ Bool freezeTime = isTimeFrozen();
// hack to let client spin fast in network games but still do effects at the same pace. -MDC
static UnsignedInt lastFrame = ~0;
- freezeTime = freezeTime || (lastFrame == TheGameClient->getFrame());
+ freezeTime = freezeTime || (TheNetwork != NULL && lastFrame == TheGameClient->getFrame());
lastFrame = TheGameClient->getFrame();
/// @todo: I'm assuming the first view is our main 3D view.
W3DView *primaryW3DView=(W3DView *)getFirstView();
+
if (!freezeTime && TheScriptEngine->isTimeFast())
{
primaryW3DView->updateCameraMovements(); // Update camera motion effects.
- syncTime += TheW3DFrameLengthInMsec;
return;
}
@@ -1822,21 +1864,9 @@ void W3DDisplay::draw( void )
}
}
- if (!freezeTime)
- {
- /// @todo Decouple framerate from timestep
- // for now, use constant time steps to avoid animations running independent of framerate
- syncTime += TheW3DFrameLengthInMsec;
- // allow W3D to update its internals
- // WW3D::Sync( GetTickCount() );
- }
- WW3D::Sync( syncTime );
-
- // Fast & Frozen time limits the time to 33 fps.
- Int minTime = 30;
- static Int prevTime = timeGetTime(), now;
-
+ static Int now;
now=timeGetTime();
+
if (TheTacticalView->getTimeMultiplier()>1)
{
static Int timeMultiplierCounter = 1;
@@ -1846,28 +1876,9 @@ void W3DDisplay::draw( void )
timeMultiplierCounter = TheTacticalView->getTimeMultiplier();
// limit the framerate, because while fast time is on, the game logic is running as fast as it can.
}
- else
- {
- now = timeGetTime();
- prevTime = now - minTime; // do the first frame immediately.
- }
-
do {
- {
- if(TheGlobalData->m_loadScreenRender != TRUE)
- {
-
- // limit the framerate
- while(TheGlobalData->m_useFpsLimit && (now - prevTime) < minTime-1)
- {
- now = timeGetTime();
- }
- prevTime = now;
- }
- }
-
// update all views of the world - recomputes data which will affect drawing
if (DX8Wrapper::_Get_D3D_Device8() && (DX8Wrapper::_Get_D3D_Device8()->TestCooperativeLevel()) == D3D_OK)
{ //Checking if we have the device before updating views because the heightmap crashes otherwise while
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp
index ae9ab7f442..6629fb9c64 100644
--- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp
@@ -38,6 +38,7 @@
// USER INCLUDES //////////////////////////////////////////////////////////////////////////////////
#include "Common/BuildAssistant.h"
+#include "Common/GameEngine.h"
#include "Common/GlobalData.h"
#include "Common/Module.h"
#include "Common/RandomValue.h"
@@ -100,7 +101,7 @@
// 30 fps
-Int TheW3DFrameLengthInMsec = 1000/LOGICFRAMES_PER_SECOND; // default is 33msec/frame == 30fps. but we may change it depending on sys config.
+Real TheW3DFrameLengthInMsec = MSEC_PER_LOGICFRAME_REAL; // default is 33msec/frame == 30fps. but we may change it depending on sys config.
static const Int MAX_REQUEST_CACHE_SIZE = 40; // Any size larger than 10, or examine code below for changes. jkmcd.
static const Real DRAWABLE_OVERSCAN = 75.0f; ///< 3D world coords of how much to overscan in the 3D screen region
@@ -408,7 +409,9 @@ void W3DView::buildCameraTransform( Matrix3D *transform )
transform->Look_At( sourcePos, targetPos, 0 );
//WST 11/12/2002 New camera shaker system
- CameraShakerSystem.Timestep(1.0f/30.0f);
+ // TheSuperHackers @tweak The camera shaker is now decoupled from the render update.
+ const Real logicTimeScaleOverFpsRatio = TheGameEngine->getActualLogicTimeScaleOverFpsRatio();
+ CameraShakerSystem.Timestep(TheW3DFrameLengthInMsec * logicTimeScaleOverFpsRatio);
CameraShakerSystem.Update_Camera_Shaker(sourcePos, &m_shakerAngles);
transform->Rotate_X(m_shakerAngles.X);
transform->Rotate_Y(m_shakerAngles.Y);
@@ -1053,7 +1056,9 @@ Bool W3DView::updateCameraMovements()
didUpdate = true;
} else if (m_doingMoveCameraOnWaypointPath) {
m_previousLookAtPosition = *getPosition();
- moveAlongWaypointPath(TheW3DFrameLengthInMsec);
+ // TheSuperHackers @tweak The scripted camera movement is now decoupled from the render update.
+ const Real logicTimeScaleOverFpsRatio = TheGameEngine->getActualLogicTimeScaleOverFpsRatio();
+ moveAlongWaypointPath(TheW3DFrameLengthInMsec * logicTimeScaleOverFpsRatio);
didUpdate = true;
}
if (m_doingScriptedCameraLock)
@@ -1077,6 +1082,50 @@ void W3DView::updateView(void)
UPDATE();
}
+// TheSuperHackers @tweak xezon 12/08/2025 The drawable update is no longer tied to the
+// render update, but it advanced separately for every fixed time step. This ensures that
+// things like vehicle wheels no longer spin too fast on high frame rates or keep spinning
+// on game pause.
+// The camera shaker is also no longer tied to the render update. The shake does sharp shakes
+// on every fixed time step, and is not intended to have linear interpolation during the
+// render update.
+void W3DView::stepView()
+{
+ //
+ // Process camera shake
+ //
+ if (m_shakeIntensity > 0.01f)
+ {
+ m_shakeOffset.x = m_shakeIntensity * m_shakeAngleCos;
+ m_shakeOffset.y = m_shakeIntensity * m_shakeAngleSin;
+
+ // fake a stiff spring/damper
+ const Real dampingCoeff = 0.75f;
+ m_shakeIntensity *= dampingCoeff;
+
+ // spring is so "stiff", it pulls 180 degrees opposite each frame
+ m_shakeAngleCos = -m_shakeAngleCos;
+ m_shakeAngleSin = -m_shakeAngleSin;
+ }
+ else
+ {
+ m_shakeIntensity = 0.0f;
+ m_shakeOffset.x = 0.0f;
+ m_shakeOffset.y = 0.0f;
+ }
+
+ if (TheScriptEngine->isTimeFast()) {
+ return; // don't draw - makes it faster :) jba.
+ }
+
+ Region3D axisAlignedRegion;
+ getAxisAlignedViewRegion(axisAlignedRegion);
+
+ // render all of the visible Drawables
+ /// @todo this needs to use a real region partition or something
+ TheGameClient->iterateDrawablesInRegion( &axisAlignedRegion, drawDrawable, this );
+}
+
//DECLARE_PERF_TIMER(W3DView_updateView)
void W3DView::update(void)
{
@@ -1289,29 +1338,11 @@ void W3DView::update(void)
}
//
// Process camera shake
- /// @todo Make this framerate-independent
//
if (m_shakeIntensity > 0.01f)
{
- m_shakeOffset.x = m_shakeIntensity * m_shakeAngleCos;
- m_shakeOffset.y = m_shakeIntensity * m_shakeAngleSin;
-
- // fake a stiff spring/damper
- const Real dampingCoeff = 0.75f;
- m_shakeIntensity *= dampingCoeff;
-
- // spring is so "stiff", it pulls 180 degrees opposite each frame
- m_shakeAngleCos = -m_shakeAngleCos;
- m_shakeAngleSin = -m_shakeAngleSin;
-
recalcCamera = true;
}
- else
- {
- m_shakeIntensity = 0.0f;
- m_shakeOffset.x = 0.0f;
- m_shakeOffset.y = 0.0f;
- }
//Process New C3 Camera Shaker system
if (CameraShakerSystem.IsCameraShaking())
@@ -1380,14 +1411,6 @@ void W3DView::update(void)
// Give the terrain a chance to refresh animaing (Seismic) regions, if any.
TheTerrainVisual->updateSeismicSimulations();
#endif
-
- Region3D axisAlignedRegion;
- getAxisAlignedViewRegion(axisAlignedRegion);
-
- // render all of the visible Drawables
- /// @todo this needs to use a real region partition or something
- if (WW3D::Get_Frame_Time()) //make sure some time actually elapsed
- TheGameClient->iterateDrawablesInRegion( &axisAlignedRegion, drawDrawable, this );
}
//-------------------------------------------------------------------------------------------------
@@ -3092,7 +3115,7 @@ void W3DView::pitchCameraOneFrame(void)
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
-void W3DView::moveAlongWaypointPath(Int milliseconds)
+void W3DView::moveAlongWaypointPath(Real milliseconds)
{
m_mcwpInfo.elapsedTimeMilliseconds += milliseconds;
if (TheGlobalData->m_disableCameraMovement) {
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp
index 44d0353acc..65ba591a26 100644
--- a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp
+++ b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp
@@ -77,8 +77,6 @@
#include "W3DDevice/Common/W3DFunctionLexicon.h"
#include "W3DDevice/GameClient/W3DGameWindowManager.h"
-#include "W3DDevice/GameClient/W3DDisplay.h"
-#include "W3DDevice/GameClient/W3DGameWindowManager.h"
#include "W3DDevice/GameClient/W3DGameFont.h"
#include "W3DDevice/GameClient/W3DDisplayStringManager.h"
#include "GameClient/Keyboard.h"
diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp
index 04ae90a499..4efb18e36a 100644
--- a/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp
+++ b/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp
@@ -183,6 +183,7 @@ class PlaceholderView : public View
virtual void drawView( void ) {}; ///< Render the world visible in this view.
virtual void updateView( void ) {}; ///< Render the world visible in this view.
+ virtual void stepView() {}; ///< Update view for every fixed time step
virtual void setZoomLimited( Bool limit ) {} ///< limit the zoom height
virtual Bool isZoomLimited( void ) { return TRUE; } ///< get status of zoom limit