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
+