From ec764883478ed2983271b84de6f27150c3e09319 Mon Sep 17 00:00:00 2001 From: Xenius97 Date: Sat, 12 Sep 2026 17:45:06 +0200 Subject: [PATCH 1/3] Implement water wave height sync --- Client/game_sa/CWaterManagerSA.cpp | 6 +++ Client/game_sa/CWaterManagerSA.h | 1 + Client/mods/deathmatch/logic/CClientGame.cpp | 4 ++ .../deathmatch/logic/CClientWaterManager.cpp | 41 +++++++++++++++++++ .../deathmatch/logic/CClientWaterManager.h | 7 ++++ Client/mods/deathmatch/logic/CNetAPI.cpp | 5 +++ .../mods/deathmatch/logic/CPacketHandler.cpp | 3 ++ Client/sdk/game/CWaterManager.h | 1 + .../logic/packets/CMapInfoPacket.cpp | 7 ++++ .../logic/packets/CReturnSyncPacket.cpp | 7 ++++ Shared/sdk/net/bitstream.h | 4 ++ 11 files changed, 86 insertions(+) diff --git a/Client/game_sa/CWaterManagerSA.cpp b/Client/game_sa/CWaterManagerSA.cpp index 433e05fde87..7a07c342154 100644 --- a/Client/game_sa/CWaterManagerSA.cpp +++ b/Client/game_sa/CWaterManagerSA.cpp @@ -818,6 +818,12 @@ float CWaterManagerSA::GetWaveLevel() return *(float*)VAR_WaveLevel; } +void CWaterManagerSA::SetWavePhase(DWORD milliseconds) +{ + const DWORD gameTime = *reinterpret_cast(0xB7CB84); + *reinterpret_cast(0xC228A4) = gameTime - milliseconds; +} + void CWaterManagerSA::SetWaveLevel(float fWaveLevel) { if (fWaveLevel >= 0.0f) diff --git a/Client/game_sa/CWaterManagerSA.h b/Client/game_sa/CWaterManagerSA.h index db189607c24..b90005f11d9 100644 --- a/Client/game_sa/CWaterManagerSA.h +++ b/Client/game_sa/CWaterManagerSA.h @@ -166,6 +166,7 @@ class CWaterManagerSA : public CWaterManager float GetWaveLevel(); void SetWaveLevel(float fWaveLevel); + void SetWavePhase(DWORD milliseconds) override; void SetWaterDrawnLast(bool bEnable); bool IsWaterDrawnLast(); diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6ed7e017179..f06bbe1acf5 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3923,6 +3923,10 @@ void CClientGame::PreRenderSkyHandler() void CClientGame::PreWeatherUpdateHandler() { + // CTimer has advanced, but water physics and rendering have not run yet. + // Refresh even after a long frame or pause, using the shared real-time phase. + m_pManager->GetWaterManager()->UpdateWavePhase(); + // Fix #4803: Set MTA's weather types and zero InterpolationValue BEFORE // CWeather::Update runs. Zeroing InterpolationValue prevents the wrap branch // from ever firing (engine_interp >= 0 is always true), so the engine computes diff --git a/Client/mods/deathmatch/logic/CClientWaterManager.cpp b/Client/mods/deathmatch/logic/CClientWaterManager.cpp index b25c110b9ab..129b223975d 100644 --- a/Client/mods/deathmatch/logic/CClientWaterManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterManager.cpp @@ -9,9 +9,50 @@ *****************************************************************************/ #include "StdInc.h" +#include +#include using std::list; +void CClientWaterManager::ReceiveWaveSync(NetBitStreamInterface& stream) +{ + unsigned int timeLow{}, timeHigh{}; + if (!stream.Read(timeLow) || !stream.Read(timeHigh)) + return; + + const auto serverTime = (static_cast(timeHigh) << 32) | timeLow; + if (serverTime <= m_lastWaveServerTime) + return; + + // Use the existing transport ping; no separate clock request is needed. + const auto now = GetTickCount64_(); + m_waveTargetTimeOffset = static_cast(serverTime) + std::max(0, g_pNet->GetPing()) * 0.5 - static_cast(now); + if (!m_lastWaveServerTime) + { + m_waveTimeOffset = m_waveTargetTimeOffset; + m_lastWaveUpdate = now; + } + m_lastWaveServerTime = serverTime; +} + +void CClientWaterManager::UpdateWavePhase() +{ + if (!m_lastWaveServerTime) + return; + + const auto now = GetTickCount64_(); + + // Correct at at most 2% so packet jitter cannot snap water or buoyancy. + // Real elapsed time keeps different FPS/game speeds on the same wave clock. + const double correction = std::max(0LL, now - m_lastWaveUpdate) * 0.02; + m_waveTimeOffset += std::clamp(m_waveTargetTimeOffset - m_waveTimeOffset, -correction, correction); + m_lastWaveUpdate = now; + + // 105000 ms is the common period of GTA's 5000/3500/3000 ms waves. + const auto phase = static_cast(std::fmod(static_cast(now) + m_waveTimeOffset, 105000.0)); + g_pGame->GetWaterManager()->SetWavePhase(phase); +} + CClientWaterManager::CClientWaterManager(CClientManager* pManager) { m_pManager = pManager; diff --git a/Client/mods/deathmatch/logic/CClientWaterManager.h b/Client/mods/deathmatch/logic/CClientWaterManager.h index e072b917325..33d303229f7 100644 --- a/Client/mods/deathmatch/logic/CClientWaterManager.h +++ b/Client/mods/deathmatch/logic/CClientWaterManager.h @@ -14,6 +14,7 @@ class CClientWaterManager; #include "CClientManager.h" #include "CClientWater.h" +#include class CClientWaterManager { @@ -35,6 +36,8 @@ class CClientWaterManager float GetWaveLevel(); void SetWaveLevel(float fWaveLevel); + void UpdateWavePhase(); + void ReceiveWaveSync(NetBitStreamInterface& stream); unsigned short GetDimension() { return m_usDimension; }; void SetDimension(unsigned short usDimension); @@ -54,4 +57,8 @@ class CClientWaterManager std::list m_List; bool m_bDontRemoveFromList; unsigned short m_usDimension; + std::uint64_t m_lastWaveServerTime = 0; + long long m_lastWaveUpdate = 0; + double m_waveTimeOffset = 0; + double m_waveTargetTimeOffset = 0; }; diff --git a/Client/mods/deathmatch/logic/CNetAPI.cpp b/Client/mods/deathmatch/logic/CNetAPI.cpp index c6c454155ab..4331b323320 100644 --- a/Client/mods/deathmatch/logic/CNetAPI.cpp +++ b/Client/mods/deathmatch/logic/CNetAPI.cpp @@ -214,6 +214,11 @@ bool CNetAPI::ProcessPacket(unsigned char bytePacketID, NetBitStreamInterface& B m_bVehicleLastReturn = false; } + // The server clock travels with the existing acknowledgement, so + // waves need neither a separate packet nor a client sync authority. + if (BitStream.Can(eBitStreamVersion::WaterWaveSync)) + m_pManager->GetWaterManager()->ReceiveWaveSync(BitStream); + // Remember the last return sync time m_ulLastSyncReturnTime = CClientTime::GetTime(); m_bStoredReturnSync = true; diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index e6a08146871..eab865c2486 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -2690,6 +2690,9 @@ void CPacketHandler::Packet_MapInfo(NetBitStreamInterface& bitStream) bitStream.ReadBit(bOcclusionsEnabled); g_pGame->GetWorld()->SetOcclusionsEnabled(bOcclusionsEnabled); + + if (bitStream.Can(eBitStreamVersion::WaterWaveSync)) + g_pClientGame->GetManager()->GetWaterManager()->ReceiveWaveSync(bitStream); } void CPacketHandler::Packet_PartialPacketInfo(NetBitStreamInterface& bitStream) diff --git a/Client/sdk/game/CWaterManager.h b/Client/sdk/game/CWaterManager.h index e399743d0fa..27dca28a7e7 100644 --- a/Client/sdk/game/CWaterManager.h +++ b/Client/sdk/game/CWaterManager.h @@ -37,4 +37,5 @@ class CWaterManager virtual void UndoChanges(void* pChangeSource = NULL) = 0; virtual void RebuildIndex() = 0; // Call this after moving a polygon's vertices virtual void Reset() = 0; // Reset all water to SA default + virtual void SetWavePhase(DWORD milliseconds) = 0; }; diff --git a/Server/mods/deathmatch/logic/packets/CMapInfoPacket.cpp b/Server/mods/deathmatch/logic/packets/CMapInfoPacket.cpp index aceac1b129f..85256c1b6c8 100644 --- a/Server/mods/deathmatch/logic/packets/CMapInfoPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CMapInfoPacket.cpp @@ -357,5 +357,12 @@ bool CMapInfoPacket::Write(NetBitStreamInterface& BitStream) const bool bOcclusionsEnabled = g_pGame->GetOcclusionsEnabled(); BitStream.WriteBit(bOcclusionsEnabled); + if (BitStream.Can(eBitStreamVersion::WaterWaveSync)) + { + const auto now = static_cast(GetTickCount64_()); + BitStream.Write(static_cast(now)); + BitStream.Write(static_cast(now >> 32)); + } + return true; } diff --git a/Server/mods/deathmatch/logic/packets/CReturnSyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CReturnSyncPacket.cpp index d63b44d570b..cf793b085ab 100644 --- a/Server/mods/deathmatch/logic/packets/CReturnSyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CReturnSyncPacket.cpp @@ -53,6 +53,13 @@ bool CReturnSyncPacket::Write(NetBitStreamInterface& BitStream) const BitStream.Write(&position); } + if (BitStream.Can(eBitStreamVersion::WaterWaveSync)) + { + const auto now = static_cast(GetTickCount64_()); + BitStream.Write(static_cast(now)); + BitStream.Write(static_cast(now >> 32)); + } + return true; } diff --git a/Shared/sdk/net/bitstream.h b/Shared/sdk/net/bitstream.h index 6b04aa6a305..1c5c2a69784 100644 --- a/Shared/sdk/net/bitstream.h +++ b/Shared/sdk/net/bitstream.h @@ -40,6 +40,10 @@ enum class eBitStreamVersion : unsigned short // YYYY-MM-DD // Name, + // Server time in map info and return sync for water wave phase. + // 2026-09-12 + WaterWaveSync, + // This allows us to automatically increment the BitStreamVersion when things are added to this enum. // Make sure you only add things above this comment. Next, From 5b664b66abb786c68c513859dce224b67d7ed89e Mon Sep 17 00:00:00 2001 From: Xenius97 Date: Sat, 12 Sep 2026 17:54:54 +0200 Subject: [PATCH 2/3] comments --- Client/mods/deathmatch/logic/CClientWaterManager.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientWaterManager.cpp b/Client/mods/deathmatch/logic/CClientWaterManager.cpp index 129b223975d..5b8b03d6157 100644 --- a/Client/mods/deathmatch/logic/CClientWaterManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterManager.cpp @@ -24,7 +24,6 @@ void CClientWaterManager::ReceiveWaveSync(NetBitStreamInterface& stream) if (serverTime <= m_lastWaveServerTime) return; - // Use the existing transport ping; no separate clock request is needed. const auto now = GetTickCount64_(); m_waveTargetTimeOffset = static_cast(serverTime) + std::max(0, g_pNet->GetPing()) * 0.5 - static_cast(now); if (!m_lastWaveServerTime) @@ -41,14 +40,10 @@ void CClientWaterManager::UpdateWavePhase() return; const auto now = GetTickCount64_(); - - // Correct at at most 2% so packet jitter cannot snap water or buoyancy. - // Real elapsed time keeps different FPS/game speeds on the same wave clock. const double correction = std::max(0LL, now - m_lastWaveUpdate) * 0.02; m_waveTimeOffset += std::clamp(m_waveTargetTimeOffset - m_waveTimeOffset, -correction, correction); m_lastWaveUpdate = now; - // 105000 ms is the common period of GTA's 5000/3500/3000 ms waves. const auto phase = static_cast(std::fmod(static_cast(now) + m_waveTimeOffset, 105000.0)); g_pGame->GetWaterManager()->SetWavePhase(phase); } From 5ef71f00faca36b8b1066d9208ff51c8e60498f2 Mon Sep 17 00:00:00 2001 From: Xenius97 Date: Sat, 12 Sep 2026 17:56:06 +0200 Subject: [PATCH 3/3] comments --- Client/mods/deathmatch/logic/CClientGame.cpp | 1 - Client/mods/deathmatch/logic/CNetAPI.cpp | 2 -- 2 files changed, 3 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index f06bbe1acf5..2addfa7826a 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3923,7 +3923,6 @@ void CClientGame::PreRenderSkyHandler() void CClientGame::PreWeatherUpdateHandler() { - // CTimer has advanced, but water physics and rendering have not run yet. // Refresh even after a long frame or pause, using the shared real-time phase. m_pManager->GetWaterManager()->UpdateWavePhase(); diff --git a/Client/mods/deathmatch/logic/CNetAPI.cpp b/Client/mods/deathmatch/logic/CNetAPI.cpp index 4331b323320..de30e1e3edb 100644 --- a/Client/mods/deathmatch/logic/CNetAPI.cpp +++ b/Client/mods/deathmatch/logic/CNetAPI.cpp @@ -214,8 +214,6 @@ bool CNetAPI::ProcessPacket(unsigned char bytePacketID, NetBitStreamInterface& B m_bVehicleLastReturn = false; } - // The server clock travels with the existing acknowledgement, so - // waves need neither a separate packet nor a client sync authority. if (BitStream.Can(eBitStreamVersion::WaterWaveSync)) m_pManager->GetWaterManager()->ReceiveWaveSync(BitStream);