From 8f1c99ae14531ef72f5007f4bd05bd6f27c30081 Mon Sep 17 00:00:00 2001 From: Batuhan Tonga <76632145+QueryOfficial@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:29:08 +0300 Subject: [PATCH] Implement bulletsync validation checks across various components - Added validation for bullet sync geometry and damage in CNetAPI, CGame, and CPlayer classes to prevent invalid bullet sync packets. - Introduced a method in CPlayer to limit the rate of accepted bulletsync packets, enhancing anti-cheat measures. - Updated packet reading methods in CBulletsyncPacket and related classes to utilize new validation functions for improved integrity checks. - Refactored existing checks to leverage the new SyncBulletsyncValidation class for consistency and maintainability. --- .vscode/settings.json | 1 + Client/mods/deathmatch/logic/CClientPed.cpp | 8 +- Client/mods/deathmatch/logic/CNetAPI.cpp | 10 + .../deathmatch/README.weapon-sync-guard.md | 47 +++++ Server/mods/deathmatch/logic/CGame.cpp | 19 +- Server/mods/deathmatch/logic/CPlayer.cpp | 20 ++ Server/mods/deathmatch/logic/CPlayer.h | 6 + .../logic/net/CSimBulletsyncPacket.cpp | 19 +- .../logic/net/CSimPlayerManager.cpp | 38 +++- .../logic/net/CSimPlayerPuresyncPacket.cpp | 5 + .../logic/net/CSimVehiclePuresyncPacket.cpp | 6 + .../logic/packets/CBulletsyncPacket.cpp | 86 ++------- .../packets/CCustomWeaponBulletSyncPacket.cpp | 3 +- .../logic/packets/CKeysyncPacket.cpp | 7 + .../logic/packets/CPlayerPuresyncPacket.cpp | 6 + .../logic/packets/CVehiclePuresyncPacket.cpp | 6 + .../logic/SyncBulletsyncValidation.h | 172 ++++++++++++++++++ 17 files changed, 366 insertions(+), 93 deletions(-) create mode 100644 Server/mods/deathmatch/README.weapon-sync-guard.md create mode 100644 Shared/mods/deathmatch/logic/SyncBulletsyncValidation.h diff --git a/.vscode/settings.json b/.vscode/settings.json index f27f5f2511a..f0759d01ad0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,5 @@ "powershell.codeFormatting.openBraceOnSameLine": true, "powershell.codeFormatting.newLineAfterCloseBrace": false, + "C_Cpp.default.compilerPath": "c:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Tools\\MSVC\\14.51.36231\\bin\\Hostx86\\x64\\cl.exe", } diff --git a/Client/mods/deathmatch/logic/CClientPed.cpp b/Client/mods/deathmatch/logic/CClientPed.cpp index bfb2cb271cf..d2d3432383c 100644 --- a/Client/mods/deathmatch/logic/CClientPed.cpp +++ b/Client/mods/deathmatch/logic/CClientPed.cpp @@ -1014,6 +1014,9 @@ void CClientPed::SetTargetTarget(unsigned long ulDelay, const CVector& vecSource { if (!m_bIsLocalPlayer) { + if (!vecSource.IsValid() || !vecTarget.IsValid()) + return; + m_ulBeginTarget = CClientTime::GetTime(); m_ulEndTarget = m_ulBeginTarget + ulDelay; m_vecBeginSource = m_shotSyncData->m_vecShotOrigin; @@ -1021,8 +1024,9 @@ void CClientPed::SetTargetTarget(unsigned long ulDelay, const CVector& vecSource m_vecTargetSource = vecSource; m_vecTargetTarget = vecTarget; - // Grab the radius of the target circle - float fRadius = DistanceBetweenPoints3D(m_vecTargetSource, m_vecTargetTarget); + const float fRadius = DistanceBetweenPoints3D(m_vecTargetSource, m_vecTargetTarget); + if (fRadius < 0.01f) + return; // Grab the angle of the source vector and the angle of the target vector relative to the source vector that applies m_vecBeginTargetAngle.fX = acos(Clamp(-1.0f, (m_vecBeginTarget.fX - m_vecBeginSource.fX) / fRadius, 1.0f)); diff --git a/Client/mods/deathmatch/logic/CNetAPI.cpp b/Client/mods/deathmatch/logic/CNetAPI.cpp index 403240260c3..00a87ef5c93 100644 --- a/Client/mods/deathmatch/logic/CNetAPI.cpp +++ b/Client/mods/deathmatch/logic/CNetAPI.cpp @@ -10,6 +10,7 @@ *****************************************************************************/ #include +#include #include #include #include @@ -2295,6 +2296,15 @@ void CNetAPI::ReadBulletsync(CClientPlayer* player, NetBitStreamInterface& strea !end.IsValid()) return; + if (!SyncBulletsyncValidation::IsSyncedBulletSegmentNonDegenerate(start, end)) + return; + + CWeaponStat* pWeaponStat = g_pGame->GetWeaponStatManager()->GetOriginalWeaponStats(type); + const float fWeaponRange = pWeaponStat ? pWeaponStat->GetWeaponRange() : 0.0f; + if (!SyncBulletsyncValidation::IsSyncedBulletsyncGeometryAcceptable(player->GetPosition(), player->GetOccupiedVehicle() != nullptr, start, end, + fWeaponRange)) + return; + std::uint8_t order = 0; if (!stream.Read(order)) return; diff --git a/Server/mods/deathmatch/README.weapon-sync-guard.md b/Server/mods/deathmatch/README.weapon-sync-guard.md new file mode 100644 index 00000000000..ff3cc69e7ce --- /dev/null +++ b/Server/mods/deathmatch/README.weapon-sync-guard.md @@ -0,0 +1,47 @@ +# Weapon Sync Guard (Bullet Sync + Aim Sync) + +## Why this exists + +Cheats hook client `SendBulletSync` and can: + +- send many bullet-sync packets per physical shot (`DamageDividier` loops) +- forge start/end vectors (including non-finite or extreme trajectories) +- bypass `onPlayerWeaponFire` on the server while still crashing remote clients + +Root cause: the **sim thread** relays `PACKET_ID_PLAYER_BULLETSYNC` before the main thread runs full validation. Invalid packets were broadcast to nearby players even when `CBulletsyncPacket::Read()` later rejected them. + +Weapon aim data in puresync/keysync can also crash remote clients when origin ≈ target (division by zero in `SetTargetTarget`). + +## What is fixed + +### 1) Player-relative geometry (`SyncBulletsyncValidation.h`) + +Validation is derived from the shooter sync position and weapon stats: + +- **Muzzle origin** within a physical envelope around the ped/vehicle sync point (5 m on foot, 25 m in vehicle) +- **Bullet segment** length ≤ weapon range (+ lag tolerance) +- **Impact point** reachable from shooter: `origin_offset + weapon_range` +- Non-finite vectors and degenerate segments rejected + +### 2) Sim-thread parity (`CSimPlayerManager::HandleBulletSync`) + +Sim relay runs the same checks as `CBulletsyncPacket::Read()` **before** broadcasting: + +- spawned/alive player, weapon ownership + ammo +- player-relative geometry + damage validation +- per-player rate limit (25 packets / second) + +### 3) Weapon aim validation (puresync + keysync) + +Full aim sync rejected when origin/target would crash remote simulation. Applied on main and sim relay paths. + +### 4) Client defense in depth + +- `CNetAPI::ReadBulletsync` drops invalid geometry +- `CClientPed::SetTargetTarget` ignores invalid/degenerate aim data + +## Residual limits + +- Rate limit is per-second, not per-weapon fire rate from stats +- Compromised clients can still send sub-threshold values; server relay rejection is the meaningful fix +- Custom weapon bullet sync validates on main thread only (no sim fast path) diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index 5a30b0ed611..c79eada2ed6 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "CGame.h" +#include "SyncBulletsyncValidation.h" #ifdef WIN32 #include @@ -2559,19 +2560,13 @@ void CGame::Packet_Bulletsync(CBulletsyncPacket& packet) if (player->GetWeaponTotalAmmo(slot) <= 0) return; - // Note: Don't check ammo in clip here - it can be out of sync due to network timing - // The total ammo check above is sufficient - const auto stat = CWeaponStatManager::GetSkillStatIndex(packet.m_weapon); const auto level = player->GetPlayerStat(stat); auto* stats = g_pGame->GetWeaponStatManager()->GetWeaponStatsFromSkillLevel(packet.m_weapon, level); + const float fWeaponRange = stats->GetWeaponRange(); - const float distanceSq = (packet.m_start - packet.m_end).LengthSquared(); - const float range = stats->GetWeaponRange(); - const float rangeSq = range * range; - - const float maxRangeSq = rangeSq * 1.1f; // 10% tolerance for floating point - if (distanceSq > maxRangeSq) + if (!SyncBulletsyncValidation::IsSyncedBulletsyncPacketAcceptable(player->GetPosition(), player->GetOccupiedVehicle() != nullptr, packet.m_start, + packet.m_end, packet.m_damage, packet.m_zone, packet.m_damaged, fWeaponRange)) return; CLuaArguments args; @@ -2612,6 +2607,12 @@ void CGame::Packet_WeaponBulletsync(CCustomWeaponBulletSyncPacket& packet) if (weapon->GetClipAmmo() <= 0) return; + CWeaponStat* pWeaponStat = g_pGame->GetWeaponStatManager()->GetOriginalWeaponStats(weapon->GetWeaponType()); + const float fWeaponRange = pWeaponStat ? pWeaponStat->GetWeaponRange() : 0.0f; + if (!SyncBulletsyncValidation::IsSyncedBulletsyncGeometryAcceptable(player->GetPosition(), player->GetOccupiedVehicle() != nullptr, packet.m_start, + packet.m_end, fWeaponRange)) + return; + CLuaArguments args; args.PushElement(player); diff --git a/Server/mods/deathmatch/logic/CPlayer.cpp b/Server/mods/deathmatch/logic/CPlayer.cpp index 35641fe7c9b..69cf61dce2a 100644 --- a/Server/mods/deathmatch/logic/CPlayer.cpp +++ b/Server/mods/deathmatch/logic/CPlayer.cpp @@ -10,6 +10,7 @@ *****************************************************************************/ #include "StdInc.h" +#include "SyncBulletsyncValidation.h" #include "CPlayer.h" #include "CElementRefManager.h" #include "CGame.h" @@ -1112,6 +1113,25 @@ void CPlayer::SetPosition(const CVector& vecPosition) CElement::SetPosition(vecPosition); } +bool CPlayer::TryAcceptBulletsync() noexcept +{ + using namespace SyncBulletsyncValidation; + + const unsigned long long ullNow = GetTickCount64_(); + + if (m_ullBulletsyncWindowStartMs == 0 || ullNow - m_ullBulletsyncWindowStartMs >= RATE_WINDOW_MS) + { + m_ullBulletsyncWindowStartMs = ullNow; + m_uiBulletsyncCountInWindow = 0; + } + + if (m_uiBulletsyncCountInWindow >= MAX_PACKETS_PER_SECOND) + return false; + + ++m_uiBulletsyncCountInWindow; + return true; +} + 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..7510637155e 100644 --- a/Server/mods/deathmatch/logic/CPlayer.h +++ b/Server/mods/deathmatch/logic/CPlayer.h @@ -263,6 +263,9 @@ class CPlayer final : public CPed, public CClient bool GetTeleported() const noexcept { return m_teleported; } void SetTeleported(bool state) noexcept { m_teleported = state; } + // Anti-cheat: cap forged bullet-sync floods (e.g. cheat loops SendBulletSync many times per shot). + bool TryAcceptBulletsync() noexcept; + protected: bool ReadSpecialData(const int iLine) override { return true; } @@ -463,4 +466,7 @@ class CPlayer final : public CPed, public CClient SString m_strQuitReasonForLog; bool m_teleported = false; + + unsigned long long m_ullBulletsyncWindowStartMs = 0; + unsigned int m_uiBulletsyncCountInWindow = 0; }; diff --git a/Server/mods/deathmatch/logic/net/CSimBulletsyncPacket.cpp b/Server/mods/deathmatch/logic/net/CSimBulletsyncPacket.cpp index 1d5c4735e95..5d37b1e6adc 100644 --- a/Server/mods/deathmatch/logic/net/CSimBulletsyncPacket.cpp +++ b/Server/mods/deathmatch/logic/net/CSimBulletsyncPacket.cpp @@ -11,6 +11,8 @@ #include "SimHeaders.h" #include "CPickupManager.h" #include "CWeaponStatManager.h" +#include "CElementIDs.h" +#include "SyncBulletsyncValidation.h" CSimBulletsyncPacket::CSimBulletsyncPacket(ElementID id) : m_id(id) { @@ -27,17 +29,26 @@ bool CSimBulletsyncPacket::Read(NetBitStreamInterface& stream) if (!stream.Read(reinterpret_cast(&m_cache.start), sizeof(CVector)) || !stream.Read(reinterpret_cast(&m_cache.end), sizeof(CVector))) return false; - if (!m_cache.start.IsValid() || !m_cache.end.IsValid()) + if (!SyncBulletsyncValidation::IsSyncedBulletSegmentNonDegenerate(m_cache.start, m_cache.end)) return false; if (!stream.Read(m_cache.order)) return false; + m_cache.damage = 0.0f; + m_cache.zone = 0; + m_cache.damaged = INVALID_ELEMENT_ID; + if (stream.ReadBit()) { - stream.Read(m_cache.damage); - stream.Read(m_cache.zone); - stream.Read(m_cache.damaged); + if (!stream.Read(m_cache.damage) || !stream.Read(m_cache.zone) || !stream.Read(m_cache.damaged)) + return false; + + if (!SyncBulletsyncValidation::IsSyncedBulletDamageAcceptable(m_cache.damage, m_cache.zone, m_cache.damaged)) + return false; + + if (m_cache.damaged != INVALID_ELEMENT_ID && !CElementIDs::GetElement(m_cache.damaged)) + return false; } return true; diff --git a/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp b/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp index 6ad6c25120a..1f21c9801b0 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp +++ b/Server/mods/deathmatch/logic/net/CSimPlayerManager.cpp @@ -9,6 +9,8 @@ #include "StdInc.h" #include "SimHeaders.h" +#include "SyncBulletsyncValidation.h" +#include "CWeaponNames.h" // // CSimPlayer object is created on CPlayer construction @@ -343,7 +345,15 @@ bool CSimPlayerManager::HandleBulletSync(const NetServerPlayerID& socket, NetBit LockSimSystem(); auto* player = Get(socket); - if (!player || !player->IsJoined()) + if (!player || !player->IsJoined() || !player->m_pRealPlayer) + { + UnlockSimSystem(); + return true; + } + + CPlayer* pRealPlayer = player->m_pRealPlayer; + + if (!pRealPlayer->IsSpawned() || pRealPlayer->IsDead()) { UnlockSimSystem(); return true; @@ -356,7 +366,31 @@ bool CSimPlayerManager::HandleBulletSync(const NetServerPlayerID& socket, NetBit return true; } - if (!player->m_pRealPlayer->HasWeaponType(packet->m_cache.weapon)) + const auto weaponType = static_cast(packet->m_cache.weapon); + if (!pRealPlayer->HasWeaponType(weaponType)) + { + UnlockSimSystem(); + return true; + } + + const auto slot = CWeaponNames::GetSlotFromWeapon(weaponType); + if (pRealPlayer->GetWeaponTotalAmmo(slot) <= 0) + { + UnlockSimSystem(); + return true; + } + + const bool bInVehicle = pRealPlayer->GetOccupiedVehicle() != nullptr; + const float fWeaponRange = pRealPlayer->GetWeaponRangeFromSlot(slot); + + if (!SyncBulletsyncValidation::IsSyncedBulletsyncPacketAcceptable(pRealPlayer->GetPosition(), bInVehicle, packet->m_cache.start, packet->m_cache.end, + packet->m_cache.damage, packet->m_cache.zone, packet->m_cache.damaged, fWeaponRange)) + { + UnlockSimSystem(); + return true; + } + + if (!pRealPlayer->TryAcceptBulletsync()) { UnlockSimSystem(); return true; diff --git a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp index 69f2f5e3e69..41637837535 100644 --- a/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/net/CSimPlayerPuresyncPacket.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "SimHeaders.h" #include "Utils.h" +#include "SyncBulletsyncValidation.h" #include "CWeaponNames.h" CSimPlayerPuresyncPacket::CSimPlayerPuresyncPacket(ElementID PlayerID, ushort PlayerLatency, uchar PlayerSyncTimeContext, uchar PlayerGotWeaponType, @@ -157,6 +158,10 @@ bool CSimPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) // Read the aim data only if he's shooting or aiming if (sync.isFull()) { + if (!SyncBulletsyncValidation::IsSyncedWeaponAimAcceptable(m_Cache.Position, sync.data.vecOrigin, sync.data.vecTarget, m_WeaponRange, + false)) + return false; + m_Cache.vecSniperSource = sync.data.vecOrigin; m_Cache.vecTargetting = sync.data.vecTarget; m_Cache.bIsAimFull = true; diff --git a/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp b/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp index 2beb4dd92a8..f8faffe9820 100644 --- a/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/net/CSimVehiclePuresyncPacket.cpp @@ -8,6 +8,7 @@ #include "StdInc.h" #include "SimHeaders.h" #include "Utils.h" +#include "SyncBulletsyncValidation.h" #include "CVehicleManager.h" #include "CWeaponNames.h" @@ -216,6 +217,11 @@ bool CSimVehiclePuresyncPacket::Read(NetBitStreamInterface& BitStream) SWeaponAimSync aim(m_fPlayerGotWeaponRange, true); if (!BitStream.Read(&aim)) return false; + + if (!SyncBulletsyncValidation::IsSyncedWeaponAimAcceptable(m_Cache.PlrPosition, aim.data.vecOrigin, aim.data.vecTarget, + m_fPlayerGotWeaponRange, true)) + return false; + m_Cache.fAimDirection = aim.data.fArm; m_Cache.vecSniperSource = aim.data.vecOrigin; m_Cache.vecTargetting = aim.data.vecTarget; diff --git a/Server/mods/deathmatch/logic/packets/CBulletsyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CBulletsyncPacket.cpp index a185a2d892b..9b6d7b208a0 100644 --- a/Server/mods/deathmatch/logic/packets/CBulletsyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CBulletsyncPacket.cpp @@ -16,6 +16,7 @@ #include "CElementIDs.h" #include "CElement.h" #include "CWeaponNames.h" +#include "SyncBulletsyncValidation.h" CBulletsyncPacket::CBulletsyncPacket(CPlayer* player) : m_weapon(WEAPONTYPE_UNARMED), m_start(), m_end(), m_order(0), m_damage(0.0f), m_zone(0), m_damaged(INVALID_ELEMENT_ID) @@ -25,19 +26,7 @@ CBulletsyncPacket::CBulletsyncPacket(CPlayer* player) bool CBulletsyncPacket::IsValidVector(const CVector& vec) noexcept { - if (!vec.IsValid()) - return false; - - if (IsNaN(vec.fX)) - return false; - - if (IsNaN(vec.fY)) - return false; - - if (IsNaN(vec.fZ)) - return false; - - return true; + return SyncBulletsyncValidation::IsSyncedBulletVectorAcceptable(vec); } bool CBulletsyncPacket::IsValidWeaponId(unsigned char weaponId) noexcept @@ -47,22 +36,7 @@ bool CBulletsyncPacket::IsValidWeaponId(unsigned char weaponId) noexcept bool CBulletsyncPacket::ValidateTrajectory() const noexcept { - const float dx = m_end.fX - m_start.fX; - const float dy = m_end.fY - m_start.fY; - const float dz = m_end.fZ - m_start.fZ; - - const float movementSq = (dx * dx) + (dy * dy) + (dz * dz); - - if (IsNaN(movementSq)) - return false; - - if (movementSq < MIN_DISTANCE_SQ) - return false; - - if (movementSq > MAX_DISTANCE_SQ) - return false; - - return true; + return SyncBulletsyncValidation::IsSyncedBulletSegmentNonDegenerate(m_start, m_end); } void CBulletsyncPacket::ResetDamageData() noexcept @@ -113,32 +87,12 @@ bool CBulletsyncPacket::ReadOptionalDamage(NetBitStreamInterface& stream) stream.Read(m_zone); stream.Read(m_damaged); - if (IsNaN(m_damage)) - { - ResetDamageData(); - return false; - } - - if (m_damage < 0.0f || m_damage > MAX_DAMAGE) + if (!SyncBulletsyncValidation::IsSyncedBulletDamageAcceptable(m_damage, m_zone, m_damaged)) { ResetDamageData(); return false; } - if (m_zone > MAX_BODY_ZONE) - { - ResetDamageData(); - return false; - } - - if (m_damaged == 0) - { - ResetDamageData(); - return false; - } - - // Check that target element exists (if specified) - // Note: m_damaged can be INVALID_ELEMENT_ID when shooting at ground/world if (m_damaged != INVALID_ELEMENT_ID) { CElement* pElement = CElementIDs::GetElement(m_damaged); @@ -147,7 +101,6 @@ bool CBulletsyncPacket::ReadOptionalDamage(NetBitStreamInterface& stream) ResetDamageData(); return false; } - // Element exists } return true; @@ -161,44 +114,27 @@ bool CBulletsyncPacket::Read(NetBitStreamInterface& stream) CPlayer* pPlayer = static_cast(m_pSourceElement); if (pPlayer) { - // Check if player is spawned and alive if (!pPlayer->IsSpawned() || pPlayer->IsDead()) return false; - - // Check player position is reasonable relative to bullet start - const CVector& playerPos = pPlayer->GetPosition(); - const float maxShootDistance = 50.0f; // Max distance from player to bullet start - - // This check will be done after we read positions } if (!ReadWeaponAndPositions(stream)) return false; - // Now validate player position relative to shot origin if (pPlayer) { - const CVector& playerPos = pPlayer->GetPosition(); - float dx = m_start.fX - playerPos.fX; - float dy = m_start.fY - playerPos.fY; - float dz = m_start.fZ - playerPos.fZ; - float distSq = dx * dx + dy * dy + dz * dz; - - // Allow larger distance if player is in vehicle (vehicle guns like Hunter have offsets of ~5m, - // plus vehicle size, plus network lag compensation) - const float maxShootDistanceSq = pPlayer->GetOccupiedVehicle() ? (100.0f * 100.0f) : (50.0f * 50.0f); - if (distSq > maxShootDistanceSq) - return false; + const auto type = static_cast(m_weapon); + const auto slot = CWeaponNames::GetSlotFromWeapon(type); - // Check if player has this weapon - if (!pPlayer->HasWeaponType(static_cast(m_weapon))) + if (!pPlayer->HasWeaponType(type)) return false; - // Check if weapon has ammo - const auto type = static_cast(m_weapon); - const auto slot = CWeaponNames::GetSlotFromWeapon(type); if (pPlayer->GetWeaponTotalAmmo(slot) <= 0) return false; + + if (!SyncBulletsyncValidation::IsSyncedBulletsyncGeometryAcceptable(pPlayer->GetPosition(), pPlayer->GetOccupiedVehicle() != nullptr, m_start, + m_end, pPlayer->GetWeaponRangeFromSlot(slot))) + return false; } if (!stream.Read(m_order)) diff --git a/Server/mods/deathmatch/logic/packets/CCustomWeaponBulletSyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CCustomWeaponBulletSyncPacket.cpp index 83b37ef062f..850820718a2 100644 --- a/Server/mods/deathmatch/logic/packets/CCustomWeaponBulletSyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CCustomWeaponBulletSyncPacket.cpp @@ -12,6 +12,7 @@ #include "net/SyncStructures.h" #include "CPlayer.h" #include "lua/CLuaFunctionParseHelpers.h" +#include "SyncBulletsyncValidation.h" CCustomWeaponBulletSyncPacket::CCustomWeaponBulletSyncPacket(CPlayer* player) { @@ -32,7 +33,7 @@ bool CCustomWeaponBulletSyncPacket::Read(NetBitStreamInterface& stream) if (!stream.Read(reinterpret_cast(&m_start), sizeof(CVector)) || !stream.Read(reinterpret_cast(&m_end), sizeof(CVector))) return false; - if (!m_start.IsValid() || !m_end.IsValid()) + if (!SyncBulletsyncValidation::IsSyncedBulletSegmentNonDegenerate(m_start, m_end)) return false; if (!stream.Read(m_order)) diff --git a/Server/mods/deathmatch/logic/packets/CKeysyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CKeysyncPacket.cpp index 0987ab189fc..bbc8fdbbd42 100644 --- a/Server/mods/deathmatch/logic/packets/CKeysyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CKeysyncPacket.cpp @@ -16,6 +16,7 @@ #include "CVehicleManager.h" #include "net/SyncStructures.h" #include "Utils.h" +#include "SyncBulletsyncValidation.h" CKeysyncPacket::CKeysyncPacket(CPlayer* pPlayer) { @@ -106,6 +107,12 @@ bool CKeysyncPacket::Read(NetBitStreamInterface& BitStream) SWeaponAimSync aim(fWeaponRange); if (!BitStream.Read(&aim)) return false; + + if (bWeaponCorrect && + !SyncBulletsyncValidation::IsSyncedWeaponAimAcceptable(pSourcePlayer->GetPosition(), aim.data.vecOrigin, aim.data.vecTarget, + fWeaponRange, pSourcePlayer->GetOccupiedVehicle() != nullptr)) + return false; + pSourcePlayer->SetSniperSourceVector(aim.data.vecOrigin); pSourcePlayer->SetTargettingVector(aim.data.vecTarget); diff --git a/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CPlayerPuresyncPacket.cpp index cb05c2138f7..4ca3b9b855a 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 "SyncBulletsyncValidation.h" #include "CTickRateSettings.h" #include @@ -279,6 +280,11 @@ bool CPlayerPuresyncPacket::Read(NetBitStreamInterface& BitStream) // Read the aim data only if he's shooting or aiming if (sync.isFull()) { + if (!SyncBulletsyncValidation::IsSyncedWeaponAimAcceptable( + position.data.vecPosition, sync.data.vecOrigin, sync.data.vecTarget, fWeaponRange, + pSourcePlayer->GetOccupiedVehicle() != nullptr)) + return false; + pSourcePlayer->SetSniperSourceVector(sync.data.vecOrigin); pSourcePlayer->SetTargettingVector(sync.data.vecTarget); } diff --git a/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp index 6f1cbdca3df..ceb9d2a8c4e 100644 --- a/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CVehiclePuresyncPacket.cpp @@ -16,6 +16,7 @@ #include "CTrainTrackManager.h" #include "CWeaponNames.h" #include "Utils.h" +#include "SyncBulletsyncValidation.h" #include "lua/CLuaFunctionParseHelpers.h" #include "net/SyncStructures.h" @@ -378,6 +379,11 @@ bool CVehiclePuresyncPacket::Read(NetBitStreamInterface& BitStream) SWeaponAimSync aim(fWeaponRange, true); if (!BitStream.Read(&aim)) return false; + + if (!SyncBulletsyncValidation::IsSyncedWeaponAimAcceptable(pSourcePlayer->GetPosition(), aim.data.vecOrigin, aim.data.vecTarget, + fWeaponRange, true)) + return false; + pSourcePlayer->SetAimDirection(aim.data.fArm); pSourcePlayer->SetSniperSourceVector(aim.data.vecOrigin); pSourcePlayer->SetTargettingVector(aim.data.vecTarget); diff --git a/Shared/mods/deathmatch/logic/SyncBulletsyncValidation.h b/Shared/mods/deathmatch/logic/SyncBulletsyncValidation.h new file mode 100644 index 00000000000..355c8bdf97d --- /dev/null +++ b/Shared/mods/deathmatch/logic/SyncBulletsyncValidation.h @@ -0,0 +1,172 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/SyncBulletsyncValidation.h + * PURPOSE: Player-relative bullet sync and weapon-aim validation + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once + +#include "CCommon.h" +#include "CVector.h" +#include + +namespace SyncBulletsyncValidation +{ + // Degenerate segment guard (SetTargetTarget divides by origin-target distance on clients). + inline constexpr float MIN_TRAJECTORY_DISTANCE_SQ = 0.0001f; + + // Physical muzzle/mount offsets from the ped or seated sync point — not gameplay caps. + inline constexpr float PED_WEAPON_ORIGIN_MAX_OFFSET = 5.0f; + inline constexpr float VEHICLE_WEAPON_ORIGIN_MAX_OFFSET = 25.0f; + + inline constexpr float MAX_DAMAGE = 200.0f; + inline constexpr unsigned char MAX_BODY_ZONE = 9; + inline constexpr float WEAPON_RANGE_TOLERANCE = 1.15f; + inline constexpr unsigned int MAX_PACKETS_PER_SECOND = 25; + inline constexpr unsigned long long RATE_WINDOW_MS = 1000; + + inline float DistanceSquared3D(const CVector& vecA, const CVector& vecB) noexcept + { + const float fDx = vecA.fX - vecB.fX; + const float fDy = vecA.fY - vecB.fY; + const float fDz = vecA.fZ - vecB.fZ; + return (fDx * fDx) + (fDy * fDy) + (fDz * fDz); + } + + inline bool IsNaN(float fValue) noexcept + { + return fValue != fValue; + } + + inline bool IsSyncedBulletVectorAcceptable(const CVector& vec) noexcept + { + return vec.IsValid(); + } + + inline float GetMaxBulletOriginOffsetFromPlayer(bool bInVehicle) noexcept + { + return bInVehicle ? VEHICLE_WEAPON_ORIGIN_MAX_OFFSET : PED_WEAPON_ORIGIN_MAX_OFFSET; + } + + inline float GetMaxBulletReachFromPlayer(bool bInVehicle, float fWeaponRange) noexcept + { + const float fRange = fWeaponRange > 0.0f ? fWeaponRange * WEAPON_RANGE_TOLERANCE : 0.0f; + return GetMaxBulletOriginOffsetFromPlayer(bInVehicle) + fRange; + } + + inline bool IsSyncedBulletSegmentNonDegenerate(const CVector& vecStart, const CVector& vecEnd) noexcept + { + if (!IsSyncedBulletVectorAcceptable(vecStart) || !IsSyncedBulletVectorAcceptable(vecEnd)) + return false; + + const float fMovementSq = DistanceSquared3D(vecStart, vecEnd); + + if (IsNaN(fMovementSq)) + return false; + + return fMovementSq >= MIN_TRAJECTORY_DISTANCE_SQ; + } + + inline bool IsSyncedBulletOriginPlausible(const CVector& vecOrigin, const CVector& vecPlayerPosition, bool bInVehicle) noexcept + { + if (!IsSyncedBulletVectorAcceptable(vecOrigin) || !IsSyncedBulletVectorAcceptable(vecPlayerPosition)) + return false; + + const float fMaxOffset = GetMaxBulletOriginOffsetFromPlayer(bInVehicle); + return DistanceSquared3D(vecOrigin, vecPlayerPosition) <= (fMaxOffset * fMaxOffset); + } + + inline bool IsSyncedBulletImpactPlausible(const CVector& vecImpact, const CVector& vecPlayerPosition, bool bInVehicle, float fWeaponRange) noexcept + { + if (!IsSyncedBulletVectorAcceptable(vecImpact) || !IsSyncedBulletVectorAcceptable(vecPlayerPosition)) + return false; + + const float fMaxReach = GetMaxBulletReachFromPlayer(bInVehicle, fWeaponRange); + return DistanceSquared3D(vecImpact, vecPlayerPosition) <= (fMaxReach * fMaxReach); + } + + inline bool IsSyncedBulletSegmentWithinWeaponRange(const CVector& vecStart, const CVector& vecEnd, float fWeaponRange) noexcept + { + if (!IsSyncedBulletSegmentNonDegenerate(vecStart, vecEnd)) + return false; + + if (fWeaponRange <= 0.0f) + return true; + + const float fMaxRange = fWeaponRange * WEAPON_RANGE_TOLERANCE; + return DistanceSquared3D(vecStart, vecEnd) <= (fMaxRange * fMaxRange); + } + + inline bool IsSyncedBulletsyncGeometryAcceptable(const CVector& vecPlayerPosition, bool bInVehicle, const CVector& vecStart, const CVector& vecEnd, + float fWeaponRange) noexcept + { + if (!IsSyncedBulletOriginPlausible(vecStart, vecPlayerPosition, bInVehicle)) + return false; + + if (!IsSyncedBulletSegmentWithinWeaponRange(vecStart, vecEnd, fWeaponRange)) + return false; + + if (!IsSyncedBulletImpactPlausible(vecEnd, vecPlayerPosition, bInVehicle, fWeaponRange)) + return false; + + return true; + } + + inline bool IsSyncedBulletDamageAcceptable(float fDamage, unsigned char ucZone, ElementID damagedElementId) noexcept + { + if (IsNaN(fDamage)) + return false; + + if (fDamage <= 0.0f) + return true; + + if (fDamage > MAX_DAMAGE) + return false; + + if (ucZone > MAX_BODY_ZONE) + return false; + + if (damagedElementId == 0) + return false; + + return true; + } + + inline bool IsSyncedWeaponAimAcceptable(const CVector& vecPlayerPosition, const CVector& vecOrigin, const CVector& vecTarget, float fWeaponRange, + bool bInVehicle) noexcept + { + return IsSyncedBulletsyncGeometryAcceptable(vecPlayerPosition, bInVehicle, vecOrigin, vecTarget, fWeaponRange); + } + + inline bool IsSyncedBulletsyncPacketAcceptable(const CVector& vecPlayerPosition, bool bInVehicle, const CVector& vecStart, const CVector& vecEnd, + float fDamage, unsigned char ucZone, ElementID damagedElementId, float fWeaponRange) noexcept + { + if (!IsSyncedBulletsyncGeometryAcceptable(vecPlayerPosition, bInVehicle, vecStart, vecEnd, fWeaponRange)) + return false; + + if (!IsSyncedBulletDamageAcceptable(fDamage, ucZone, damagedElementId)) + return false; + + return true; + } + + inline bool IsSyncedBulletTrajectoryAcceptable(const CVector& vecStart, const CVector& vecEnd) noexcept + { + return IsSyncedBulletSegmentNonDegenerate(vecStart, vecEnd); + } + + inline bool IsSyncedBulletOriginNearPlayer(const CVector& vecStart, const CVector& vecPlayerPosition, bool bInVehicle) noexcept + { + return IsSyncedBulletOriginPlausible(vecStart, vecPlayerPosition, bInVehicle); + } + + inline bool IsSyncedBulletWithinWeaponRange(const CVector& vecStart, const CVector& vecEnd, float fWeaponRange) noexcept + { + return IsSyncedBulletSegmentWithinWeaponRange(vecStart, vecEnd, fWeaponRange); + } +}