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..2addfa7826a 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3923,6 +3923,9 @@ void CClientGame::PreRenderSkyHandler() void CClientGame::PreWeatherUpdateHandler() { + // 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..5b8b03d6157 100644 --- a/Client/mods/deathmatch/logic/CClientWaterManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWaterManager.cpp @@ -9,9 +9,45 @@ *****************************************************************************/ #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; + + 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_(); + const double correction = std::max(0LL, now - m_lastWaveUpdate) * 0.02; + m_waveTimeOffset += std::clamp(m_waveTargetTimeOffset - m_waveTimeOffset, -correction, correction); + m_lastWaveUpdate = now; + + 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..de30e1e3edb 100644 --- a/Client/mods/deathmatch/logic/CNetAPI.cpp +++ b/Client/mods/deathmatch/logic/CNetAPI.cpp @@ -214,6 +214,9 @@ bool CNetAPI::ProcessPacket(unsigned char bytePacketID, NetBitStreamInterface& B m_bVehicleLastReturn = false; } + 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,