diff --git a/Server/mods/deathmatch/README.sync-manipulation-guard.md b/Server/mods/deathmatch/README.sync-manipulation-guard.md new file mode 100644 index 00000000000..240f7a912d1 --- /dev/null +++ b/Server/mods/deathmatch/README.sync-manipulation-guard.md @@ -0,0 +1,97 @@ +# Sync Manipulation Guard (On-Foot) + +## Why this exists + +Some cheats inject client-side Lua that forges **outgoing player puresync** (position and/or velocity) while the attacker is **on foot**. +The server would otherwise accept and relay that sync to other clients, where the remote attacker ped is simulated with impossible movement and violently collides with nearby vehicles and players. + +Typical attack pattern: + +- attacker stays on foot +- attacker forges `api.sync.velocity(...)` (or equivalent `setElementVelocity` path) +- optionally forges `api.sync.position(...)` in the same or following packets +- velocity is only sent in some packets (`bSyncingVelocity`); position is sent every packet +- other clients receive and simulate that remote ped movement +- nearby vehicles are physically launched + +This guard rejects forged on-foot puresync at the **server packet read stage**, before state is updated or packets are relayed. + +## What is fixed + +### 1) On-foot velocity sanity check + +For player pure sync, the server rejects packets with impossible on-foot velocity magnitude: + +- setting: `player_max_sync_speed` (km/h) +- default: `360` +- `0` disables the check + +### 2) On-foot position delta check + +Cheats can omit the velocity field and forge position only. Every on-foot puresync packet is checked against a reference position: + +- last accepted puresync position when available +- otherwise the server's current element position (no unchecked "first packet" window) + +When the player has **valid vehicle contact** (`bHasContact` on a vehicle that passed the existing contact radius checks), the delta is measured in **contact-relative** coordinates instead of absolute world space. That avoids false positives when surfing on a legitimately fast vehicle, where absolute movement reflects vehicle speed rather than on-foot ped speed. + +The packet is rejected if the chosen reference position could not have reached the new position in the elapsed time at `player_max_sync_speed` (with a small margin). When no prior baseline exists, elapsed time defaults to one pure-sync tick (`iPureSync`, typically 100 ms). + +### 3) Non-finite position rejection (NaN/Inf) + +Player and vehicle pure sync position fields are rejected when coordinates are non-finite. + +### 4) Main thread and sim thread parity + +Player puresync is validated in both paths: + +- main thread: `CPlayerPuresyncPacket::Read` +- sim thread relay: `CSimPlayerPuresyncPacket::Read` + +The sim thread reads the last accepted baseline from the real `CPlayer` before validating so both paths agree on accept/reject. + +Rejected packets are neither applied to server state nor relayed to other players. + +## Legitimate bypasses + +- **Server-authorized teleport:** `SetTeleported(true)` (spawn, `setElementPosition`, etc.) skips movement checks for the next accepted packet and clears the movement baseline. +- **In vehicle (seated):** on-foot checks apply only when the player is on foot or exiting a vehicle; vehicle gameplay is not speed-capped by this feature. +- **Vehicle contact (surf / standing on roof):** position delta uses contact-relative coordinates so fast vehicles remain playable. + +## Important scope note + +This guard targets **on-foot manipulation used to launch other players' vehicles and players**. + +It does **not** introduce a general vehicle top speed limiter: + +- `vehicle_max_sync_speed` is not used +- legitimate high-speed vehicle gameplay (500–1000+ km/h) is unaffected + +## Configuration + +In `mtaserver.conf`: + +```xml +360 +``` + +Guidance: + +- Lower value: stricter anti-manipulation, higher false-positive risk +- Higher value: more permissive, weaker protection +- `0`: disabled (not recommended on public servers) + +## Residual limits (why not mathematically perfect) + +- **Threshold model:** The server does not run GTA physics; it sanity-checks client sync against a max on-foot speed. A cheater who stays just under 360 km/h (and the 1.35× margin) could still nudge movement slightly — that is a fundamental trade-off vs. jetpack, knockback, and high-ping jitter. +- **Authorized teleports:** `SetTeleported(true)` must bypass checks or legitimate warps break. +- **Compromised client binary:** A patched client can send anything; the meaningful fix is server-side relay rejection, not trusting the client. +- **`player_max_sync_speed = 0`:** Disables all checks. + +For the documented on-foot sync-forge launch exploit (R/E scripts, position-only forge, vehicle-exit reset tricks), every relay path now runs velocity + position delta validation before apply/relay. + +## Non-goals + +- Not a full anti-teleport system for all movement edge cases +- Does not replace broader anti-cheat layers +- Client-side clamping is optional defense-in-depth; server validation is the primary fix diff --git a/Server/mods/deathmatch/logic/CMainConfig.cpp b/Server/mods/deathmatch/logic/CMainConfig.cpp index a7e3ffbeede..5cb1c5546de 100644 --- a/Server/mods/deathmatch/logic/CMainConfig.cpp +++ b/Server/mods/deathmatch/logic/CMainConfig.cpp @@ -1527,6 +1527,9 @@ const std::vector& CMainConfig::GetIntSettingList() {true, true, 50, 130, 400, "unoccupied_vehicle_syncer_distance", &g_TickRateSettings.iUnoccupiedVehicleSyncerDistance, &CMainConfig::OnTickRateChange}, {true, true, 0, 30, 130, "vehicle_contact_sync_radius", &g_TickRateSettings.iVehicleContactSyncRadius, &CMainConfig::OnTickRateChange}, {true, true, 5, 100, 500, "player_teleport_alert", &g_TickRateSettings.playerTeleportAlert, &CMainConfig::OnTickRateChange}, + // Highest believable synced speed (km/h) for on-foot players. Puresync packets above this are + // dropped to stop injected ped velocities being relayed (remote vehicle launch cheats). 0 = off. + {true, true, 0, 360, 100000, "player_max_sync_speed", &g_TickRateSettings.iPlayerMaxSyncSpeed, nullptr}, {false, false, 0, 1, 2, "compact_internal_databases", &m_iCompactInternalDatabases, NULL}, {true, true, 0, 1, 2, "minclientversion_auto_update", &m_iMinClientVersionAutoUpdate, NULL}, {true, true, 0, 0, 100, "server_logic_fps_limit", &m_iServerLogicFpsLimit, NULL}, diff --git a/Server/mods/deathmatch/logic/CPed.h b/Server/mods/deathmatch/logic/CPed.h index 11da52a6422..d87b6dd5ede 100644 --- a/Server/mods/deathmatch/logic/CPed.h +++ b/Server/mods/deathmatch/logic/CPed.h @@ -255,9 +255,9 @@ class CPed : public CElement float GetGravity() { return m_fGravity; } void SetGravity(float fGravity) { m_fGravity = fGravity; } - CVehicle* GetOccupiedVehicle() { return m_pVehicle; }; - unsigned int GetOccupiedVehicleSeat() { return m_uiVehicleSeat; }; - CVehicle* SetOccupiedVehicle(CVehicle* pVehicle, unsigned int uiSeat); + CVehicle* GetOccupiedVehicle() { return m_pVehicle; }; + unsigned int GetOccupiedVehicleSeat() { return m_uiVehicleSeat; }; + virtual CVehicle* SetOccupiedVehicle(CVehicle* pVehicle, unsigned int uiSeat); unsigned int GetVehicleAction() { return m_uiVehicleAction; }; void SetVehicleAction(unsigned int uiAction); diff --git a/Server/mods/deathmatch/logic/CPlayer.cpp b/Server/mods/deathmatch/logic/CPlayer.cpp index 35641fe7c9b..5d22c87a602 100644 --- a/Server/mods/deathmatch/logic/CPlayer.cpp +++ b/Server/mods/deathmatch/logic/CPlayer.cpp @@ -1112,6 +1112,49 @@ void CPlayer::SetPosition(const CVector& vecPosition) CElement::SetPosition(vecPosition); } +void CPlayer::SetTeleported(bool state) noexcept +{ + m_teleported = state; + // Authorized warps must not inherit a stale on-foot baseline from before the teleport. + if (state) + ResetPuresyncMovementGuard(); +} + +CVehicle* CPlayer::SetOccupiedVehicle(CVehicle* pVehicle, unsigned int uiSeat) +{ + CVehicle* pResult = CPed::SetOccupiedVehicle(pVehicle, uiSeat); + // Vehicle puresync replaces on-foot puresync; drop the baseline so exiting the vehicle + // does not compare against a position from before the ride. + if (pVehicle) + ResetPuresyncMovementGuard(); + return pResult; +} + +void CPlayer::NoteAcceptedPuresyncPosition(const CVector& vecAbsolutePosition, CElement* pContactElement, const CVector& vecRelativePosition) +{ + m_vecLastAcceptedPuresyncPosition = vecAbsolutePosition; + m_ullLastAcceptedPuresyncTick = GetTickCount64_(); + m_bHasLastAcceptedPuresyncPosition = true; + m_teleported = false; + + if (pContactElement && pContactElement->GetType() == CElement::VEHICLE) + { + m_LastAcceptedPuresyncContactElementID = pContactElement->GetID(); + m_vecLastAcceptedPuresyncContactRelative = vecRelativePosition; + } + else + { + m_LastAcceptedPuresyncContactElementID = INVALID_ELEMENT_ID; + } +} + +void CPlayer::ResetPuresyncMovementGuard() noexcept +{ + m_bHasLastAcceptedPuresyncPosition = false; + m_ullLastAcceptedPuresyncTick = 0; + m_LastAcceptedPuresyncContactElementID = INVALID_ELEMENT_ID; +} + void CPlayer::SetPlayerStat(unsigned short usStat, float fValue) { m_pPlayerStatsPacket->Add(usStat, fValue); diff --git a/Server/mods/deathmatch/logic/CPlayer.h b/Server/mods/deathmatch/logic/CPlayer.h index b332f40eb12..d95ead649d5 100644 --- a/Server/mods/deathmatch/logic/CPlayer.h +++ b/Server/mods/deathmatch/logic/CPlayer.h @@ -261,7 +261,18 @@ class CPlayer final : public CPed, public CClient bool IsRedirecting() const noexcept { return m_bIsRedirecting; } bool GetTeleported() const noexcept { return m_teleported; } - void SetTeleported(bool state) noexcept { m_teleported = state; } + void SetTeleported(bool state) noexcept; + + CVehicle* SetOccupiedVehicle(CVehicle* pVehicle, unsigned int uiSeat) override; + + // Anti-cheat: last accepted on-foot puresync position used to reject forged position-only sync. + bool HasLastAcceptedPuresyncPosition() const noexcept { return m_bHasLastAcceptedPuresyncPosition; } + const CVector& GetLastAcceptedPuresyncPosition() const noexcept { return m_vecLastAcceptedPuresyncPosition; } + unsigned long long GetLastAcceptedPuresyncTick() const noexcept { return m_ullLastAcceptedPuresyncTick; } + ElementID GetLastAcceptedPuresyncContactElementID() const noexcept { return m_LastAcceptedPuresyncContactElementID; } + const CVector& GetLastAcceptedPuresyncContactRelative() const noexcept { return m_vecLastAcceptedPuresyncContactRelative; } + void NoteAcceptedPuresyncPosition(const CVector& vecAbsolutePosition, CElement* pContactElement, const CVector& vecRelativePosition); + void ResetPuresyncMovementGuard() noexcept; protected: bool ReadSpecialData(const int iLine) override { return true; } @@ -463,4 +474,10 @@ class CPlayer final : public CPed, public CClient SString m_strQuitReasonForLog; bool m_teleported = false; + + bool m_bHasLastAcceptedPuresyncPosition = false; + CVector m_vecLastAcceptedPuresyncPosition; + unsigned long long m_ullLastAcceptedPuresyncTick = 0; + ElementID m_LastAcceptedPuresyncContactElementID = INVALID_ELEMENT_ID; + CVector m_vecLastAcceptedPuresyncContactRelative; }; diff --git a/Server/mods/deathmatch/logic/net/CSimPlayer.h b/Server/mods/deathmatch/logic/net/CSimPlayer.h index 16885e303a0..06953b98fd4 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayer.h +++ b/Server/mods/deathmatch/logic/net/CSimPlayer.h @@ -65,6 +65,14 @@ class CSimPlayer // Flag bool m_bDoneFirstUpdate; + // Anti-cheat: mirror CPlayer puresync movement guard for sim-thread relay validation + bool m_bHasLastAcceptedPuresyncPosition = false; + CVector m_vecLastAcceptedPuresyncPosition; + unsigned long long m_ullLastAcceptedPuresyncTick = 0; + ElementID m_LastAcceptedPuresyncContactElementID = INVALID_ELEMENT_ID; + CVector m_vecLastAcceptedPuresyncContactRelative; + bool m_bTeleported = false; + // Interlink to CPlayer object CPlayer* m_pRealPlayer; }; diff --git a/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp b/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp index 6ad6c25120a..67517b833bc 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp +++ b/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp @@ -141,6 +141,12 @@ void CSimPlayerManager::UpdateSimPlayer(CPlayer* pPlayer) pSim->m_ucOccupiedVehicleSeat = uiSeat <= 0xFF ? static_cast(uiSeat) : 0xFF; } pSim->m_fWeaponRange = pPlayer->GetWeaponRangeFromSlot(); + pSim->m_bTeleported = pPlayer->GetTeleported(); + pSim->m_bHasLastAcceptedPuresyncPosition = pPlayer->HasLastAcceptedPuresyncPosition(); + pSim->m_vecLastAcceptedPuresyncPosition = pPlayer->GetLastAcceptedPuresyncPosition(); + pSim->m_ullLastAcceptedPuresyncTick = pPlayer->GetLastAcceptedPuresyncTick(); + pSim->m_LastAcceptedPuresyncContactElementID = pPlayer->GetLastAcceptedPuresyncContactElementID(); + pSim->m_vecLastAcceptedPuresyncContactRelative = pPlayer->GetLastAcceptedPuresyncContactRelative(); pSim->m_bVehicleHasHydraulics = pVehicle ? pVehicle->GetUpgrades()->HasUpgrade(1087) : false; pSim->m_bVehicleIsPlaneOrHeli = pVehicle ? pVehicle->GetVehicleType() == VEHICLE_PLANE || pVehicle->GetVehicleType() == VEHICLE_HELI : false; pSim->m_sharedControllerState.Copy(pPlayer->GetPad()->GetCurrentControllerState()); @@ -239,10 +245,21 @@ bool CSimPlayerManager::HandlePlayerPureSync(const NetServerPlayerID& Socket, Ne // Check is good for player pure sync if (pSourceSimPlayer && pSourceSimPlayer->IsJoined() && (!pSourceSimPlayer->m_bHasOccupiedVehicle || pSourceSimPlayer->m_bIsExitingVehicle)) { + // Keep sim-thread validation aligned with the main thread's last accepted puresync state. + if (CPlayer* pRealPlayer = pSourceSimPlayer->m_pRealPlayer) + { + pSourceSimPlayer->m_bHasLastAcceptedPuresyncPosition = pRealPlayer->HasLastAcceptedPuresyncPosition(); + pSourceSimPlayer->m_vecLastAcceptedPuresyncPosition = pRealPlayer->GetLastAcceptedPuresyncPosition(); + pSourceSimPlayer->m_ullLastAcceptedPuresyncTick = pRealPlayer->GetLastAcceptedPuresyncTick(); + pSourceSimPlayer->m_LastAcceptedPuresyncContactElementID = pRealPlayer->GetLastAcceptedPuresyncContactElementID(); + pSourceSimPlayer->m_vecLastAcceptedPuresyncContactRelative = pRealPlayer->GetLastAcceptedPuresyncContactRelative(); + pSourceSimPlayer->m_bTeleported = pRealPlayer->GetTeleported(); + } + // Read the incoming packet data - CSimPlayerPuresyncPacket* pPacket = - new CSimPlayerPuresyncPacket(pSourceSimPlayer->m_PlayerID, pSourceSimPlayer->m_usLatency, pSourceSimPlayer->m_ucSyncTimeContext, - pSourceSimPlayer->m_ucWeaponType, pSourceSimPlayer->m_fWeaponRange, pSourceSimPlayer->m_sharedControllerState); + CSimPlayerPuresyncPacket* pPacket = new CSimPlayerPuresyncPacket( + pSourceSimPlayer->m_PlayerID, pSourceSimPlayer->m_usLatency, pSourceSimPlayer->m_ucSyncTimeContext, pSourceSimPlayer->m_ucWeaponType, + pSourceSimPlayer->m_fWeaponRange, pSourceSimPlayer->m_sharedControllerState, pSourceSimPlayer); if (pPacket->Read(*BitStream)) { // Relay it to nearbyers diff --git a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp index 69f2f5e3e69..47bed0d8c1e 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp @@ -10,17 +10,21 @@ #include "StdInc.h" #include "SimHeaders.h" #include "Utils.h" +#include "SyncPuresyncValidation.h" #include "CWeaponNames.h" +#include "CTickRateSettings.h" +#include "CElementIDs.h" CSimPlayerPuresyncPacket::CSimPlayerPuresyncPacket(ElementID PlayerID, ushort PlayerLatency, uchar PlayerSyncTimeContext, uchar PlayerGotWeaponType, - float WeaponRange, CControllerState& sharedControllerState) + float WeaponRange, CControllerState& sharedControllerState, CSimPlayer* pSimPlayer) : m_PlayerID(PlayerID), m_PlayerLatency(PlayerLatency), m_PlayerSyncTimeContext(PlayerSyncTimeContext), m_PlayerGotWeaponType(PlayerGotWeaponType), m_WeaponRange(WeaponRange), - m_sharedControllerState(sharedControllerState) + m_sharedControllerState(sharedControllerState), + m_pSimPlayer(pSimPlayer) { } @@ -62,8 +66,45 @@ bool CSimPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) SPositionSync position(false); if (!BitStream.Read(&position)) return false; + + // Reject NaN/Inf coordinates before relaying them to nearby players (SPositionSync only range-checks). + if (!position.data.vecPosition.IsValid()) + return false; + m_Cache.Position = position.data.vecPosition; + CVector vecAbsolutePosition = position.data.vecPosition; + CElement* pContactElement = nullptr; + if (m_Cache.flags.data.bHasContact) + { + pContactElement = CElementIDs::GetElement(m_Cache.ContactElementID); + if (pContactElement && m_pSimPlayer && m_pSimPlayer->m_pRealPlayer) + { + CPlayer* pSourcePlayer = m_pSimPlayer->m_pRealPlayer; + int32_t radius = -1; + + if (pContactElement->GetType() == CElement::VEHICLE) + { + if (((CVehicle*)pContactElement)->GetSyncer() != pSourcePlayer) + radius = g_TickRateSettings.iVehicleContactSyncRadius; + } + + if (radius > -1 && (!IsPointNearPoint3D(pSourcePlayer->GetPosition(), pContactElement->GetPosition(), static_cast(radius)) || + pSourcePlayer->GetDimension() != pContactElement->GetDimension())) + { + pContactElement = nullptr; + vecAbsolutePosition = pSourcePlayer->GetPosition(); + } + } + } + + if (m_Cache.flags.data.bHasContact && pContactElement == nullptr && m_pSimPlayer && m_pSimPlayer->m_pRealPlayer) + vecAbsolutePosition = m_pSimPlayer->m_pRealPlayer->GetPosition(); + else if (pContactElement) + vecAbsolutePosition += pContactElement->GetPosition(); + + m_Cache.AbsolutePosition = vecAbsolutePosition; + // Player rotation SPedRotationSync rotation; if (!BitStream.Read(&rotation)) @@ -76,9 +117,41 @@ bool CSimPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) SVelocitySync velocity; if (!BitStream.Read(&velocity)) return false; + m_Cache.Velocity = velocity.data.vecVelocity; } + if (m_pSimPlayer) + { + const bool bVehicleContact = pContactElement && pContactElement->GetType() == CElement::VEHICLE; + CVector vecServerContactRelative; + CElement* pServerContactElement = nullptr; + if (m_pSimPlayer->m_pRealPlayer) + { + m_pSimPlayer->m_pRealPlayer->GetContactPosition(vecServerContactRelative); + pServerContactElement = m_pSimPlayer->m_pRealPlayer->GetContactElement(); + } + + CVector vecReferencePosition; + CVector vecNewPosition; + ResolvePuresyncMovementDelta(bVehicleContact, m_Cache.flags.data.bHasContact ? m_Cache.ContactElementID : INVALID_ELEMENT_ID, + m_pSimPlayer->m_LastAcceptedPuresyncContactElementID, m_Cache.Position, vecAbsolutePosition, + m_pSimPlayer->m_vecLastAcceptedPuresyncContactRelative, m_pSimPlayer->m_vecLastAcceptedPuresyncPosition, + vecServerContactRelative, pServerContactElement, pContactElement, m_pSimPlayer->m_bHasLastAcceptedPuresyncPosition, + m_pSimPlayer->m_pRealPlayer ? m_pSimPlayer->m_pRealPlayer->GetPosition() : vecAbsolutePosition, vecReferencePosition, + vecNewPosition); + + const unsigned long long ullElapsedMs = + m_pSimPlayer->m_bHasLastAcceptedPuresyncPosition ? GetTickCount64_() - m_pSimPlayer->m_ullLastAcceptedPuresyncTick : 0; + + if (!IsPlayerPuresyncMovementAcceptable(vecReferencePosition, ullElapsedMs, g_TickRateSettings.iPureSync, vecNewPosition, + m_Cache.flags.data.bSyncingVelocity, m_Cache.Velocity, g_TickRateSettings.iPlayerMaxSyncSpeed, + m_pSimPlayer->m_bTeleported)) + { + return false; + } + } + // Health ( stored with damage ) SPlayerHealthSync health; if (!BitStream.Read(&health)) diff --git a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.h b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.h index aba98cb7572..dd928f07729 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.h +++ b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.h @@ -34,7 +34,7 @@ class CSimPlayerPuresyncPacket : public CSimPacket ZERO_ON_NEW CSimPlayerPuresyncPacket(ElementID PlayerID, ushort PlayerLatency, uchar PlayerSyncTimeContext, uchar PlayerGotWeaponType, float WeaponRange, - CControllerState& sharedControllerState); + CControllerState& sharedControllerState, CSimPlayer* pSimPlayer); ePacketID GetPacketID() const { return PACKET_ID_PLAYER_PURESYNC; }; unsigned long GetFlags() const { return PACKET_MEDIUM_PRIORITY | PACKET_SEQUENCED; }; @@ -56,6 +56,7 @@ class CSimPlayerPuresyncPacket : public CSimPacket const uchar m_PlayerGotWeaponType; const float m_WeaponRange; CControllerState& m_sharedControllerState; + CSimPlayer* m_pSimPlayer; // Set in Read () struct @@ -64,6 +65,7 @@ class CSimPlayerPuresyncPacket : public CSimPacket SPlayerPuresyncFlags flags; ElementID ContactElementID; CVector Position; + CVector AbsolutePosition; float fRotation; CVector Velocity; float fHealth; diff --git a/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp b/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp index 2beb4dd92a8..ccddaede504 100644 --- a/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp @@ -61,6 +61,11 @@ bool CSimVehiclePuresyncPacket::Read(NetBitStreamInterface& BitStream) SPositionSync position(false); if (!BitStream.Read(&position)) return false; + + // Reject NaN/Inf coordinates before relaying them to nearby players (SPositionSync only range-checks). + if (!position.data.vecPosition.IsValid()) + return false; + m_Cache.PlrPosition = position.data.vecPosition; if (CVehicleManager::GetVehicleType(static_cast(m_Cache.iModelID)) == VEHICLE_TRAIN) diff --git a/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp index cb05c2138f7..21550a9d1dd 100644 --- a/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp @@ -14,6 +14,7 @@ #include "CElementIDs.h" #include "CWeaponNames.h" #include "Utils.h" +#include "SyncPuresyncValidation.h" #include "CTickRateSettings.h" #include @@ -83,6 +84,12 @@ bool CPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) bool positionRead = BitStream.Read(&position); const CVector vecRelativePosition = position.data.vecPosition; + // Reject NaN/Inf coordinates outright. SPositionSync only range-checks the position, but a + // non-finite value slips past that (comparisons against NaN are always false) and would be + // applied/relayed verbatim, corrupting the element and other clients. + if (positionRead && !vecRelativePosition.IsValid()) + return false; + if (positionRead && pContactElement != nullptr) { int32_t radius = -1; @@ -133,6 +140,8 @@ bool CPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) if (!positionRead) return false; + const CVector vecSyncRelativePosition = position.data.vecPosition; + if (pContactElement) { pSourcePlayer->SetContactPosition(position.data.vecPosition); @@ -159,28 +168,56 @@ bool CPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) arguments.PushNumber(position.data.vecPosition.fZ); pSourcePlayer->CallEvent("onPlayerTeleport", arguments, nullptr); } - - pSourcePlayer->SetTeleported(false); } } - pSourcePlayer->SetPosition(position.data.vecPosition); - - // Player rotation + // Read rotation and optional velocity before applying movement so forged sync can be + // rejected before it touches server state or is relayed to other clients. SPedRotationSync rotation; if (!BitStream.Read(&rotation)) return false; - pSourcePlayer->SetRotation(rotation.data.fRotation); - // Move speed vector - if (flags.data.bSyncingVelocity) + CVector vecPacketVelocity; + const bool bSyncingVelocity = flags.data.bSyncingVelocity; + if (bSyncingVelocity) { SVelocitySync velocity; if (!BitStream.Read(&velocity)) return false; - pSourcePlayer->SetVelocity(velocity.data.vecVelocity); + vecPacketVelocity = velocity.data.vecVelocity; } + const bool bOnFoot = !pSourcePlayer->GetOccupiedVehicle() || pSourcePlayer->GetVehicleAction() == CPed::VEHICLEACTION_EXITING; + if (bOnFoot) + { + const bool bVehicleContact = pContactElement && pContactElement->GetType() == CElement::VEHICLE; + CVector vecServerContactRelative; + pSourcePlayer->GetContactPosition(vecServerContactRelative); + + CVector vecReferencePosition; + CVector vecNewPosition; + ResolvePuresyncMovementDelta(bVehicleContact, pContactElement ? pContactElement->GetID() : INVALID_ELEMENT_ID, + pSourcePlayer->GetLastAcceptedPuresyncContactElementID(), vecSyncRelativePosition, position.data.vecPosition, + pSourcePlayer->GetLastAcceptedPuresyncContactRelative(), pSourcePlayer->GetLastAcceptedPuresyncPosition(), + vecServerContactRelative, pSourcePlayer->GetContactElement(), pContactElement, + pSourcePlayer->HasLastAcceptedPuresyncPosition(), pSourcePlayer->GetPosition(), vecReferencePosition, vecNewPosition); + + const unsigned long long ullElapsedMs = + pSourcePlayer->HasLastAcceptedPuresyncPosition() ? GetTickCount64_() - pSourcePlayer->GetLastAcceptedPuresyncTick() : 0; + + if (!IsPlayerPuresyncMovementAcceptable(vecReferencePosition, ullElapsedMs, g_TickRateSettings.iPureSync, vecNewPosition, bSyncingVelocity, + vecPacketVelocity, g_TickRateSettings.iPlayerMaxSyncSpeed, pSourcePlayer->GetTeleported())) + { + return false; + } + } + + pSourcePlayer->SetPosition(position.data.vecPosition); + pSourcePlayer->SetRotation(rotation.data.fRotation); + + if (bSyncingVelocity) + pSourcePlayer->SetVelocity(vecPacketVelocity); + // Health ( stored with damage ) SPlayerHealthSync health; if (!BitStream.Read(&health)) @@ -349,6 +386,9 @@ bool CPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) pSourcePlayer->CallEvent("onPlayerDamage", Arguments); } + if (bOnFoot) + pSourcePlayer->NoteAcceptedPuresyncPosition(position.data.vecPosition, pContactElement, vecSyncRelativePosition); + // Success return true; } diff --git a/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp index 6f1cbdca3df..b889b4c8ee2 100644 --- a/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp @@ -68,6 +68,12 @@ bool CVehiclePuresyncPacket::Read(NetBitStreamInterface& BitStream) SPositionSync position(false); if (!BitStream.Read(&position)) return false; + + // Reject NaN/Inf coordinates: SPositionSync only range-checks, so a non-finite value would + // otherwise be applied to the player and vehicle and relayed to everyone. + if (!position.data.vecPosition.IsValid()) + return false; + pSourcePlayer->SetPosition(position.data.vecPosition); // If the remote vehicle is a train, we want to read special train-specific data diff --git a/Server/mods/deathmatch/mtaserver.conf b/Server/mods/deathmatch/mtaserver.conf index 21b736a6bb1..522546d5d1c 100644 --- a/Server/mods/deathmatch/mtaserver.conf +++ b/Server/mods/deathmatch/mtaserver.conf @@ -170,6 +170,13 @@ Available range: 5 to 500. Default - 100 --> 100 + + 360 +