From eea1875d62a683df2b78dc5f16198d822c513b48 Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 11:39:07 +0200 Subject: [PATCH] Harden melee sync authority --- src/game/battle.cpp | 44 +++++++++++++++++++++++++++++++++++++++++ src/game/char.cpp | 25 +++++++++++++++++++++++ src/game/char.h | 5 +++++ src/game/input_main.cpp | 42 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/game/battle.cpp b/src/game/battle.cpp index c67dda1..ef26ec7 100644 --- a/src/game/battle.cpp +++ b/src/game/battle.cpp @@ -24,6 +24,27 @@ int battle_hit(LPCHARACTER ch, LPCHARACTER victim, int & iRetDam); +namespace +{ +bool battle_melee_angle_valid(LPCHARACTER ch, LPCHARACTER victim, int distance, int maxDistance, float* outRotDelta) +{ + if (!ch || !victim || !ch->IsPC() || victim->IsBuilding()) + return true; + + if (distance <= 170) + return true; + + const float desiredRotation = GetDegreeFromPositionXY(ch->GetX(), ch->GetY(), victim->GetX(), victim->GetY()); + const float rotDelta = fabs(GetDegreeDelta(ch->GetRotation(), desiredRotation)); + + if (outRotDelta) + *outRotDelta = rotDelta; + + const float allowedDelta = distance >= MAX(220, maxDistance - 40) ? 95.0f : 120.0f; + return rotDelta <= allowedDelta; +} +} + bool battle_distance_valid_by_xy(long x, long y, long tx, long ty) { long distance = DISTANCE_APPROX(x - tx, y - ty); @@ -160,6 +181,25 @@ int battle_melee_attack(LPCHARACTER ch, LPCHARACTER victim) return BATTLE_NONE; } + + float rotDelta = 0.0f; + if (!battle_melee_angle_valid(ch, victim, distance, max, &rotDelta)) + { + char szDetail[160]; + snprintf(szDetail, + sizeof(szDetail), + "rot_delta=%.1f distance=%d max=%d victim=%u", + rotDelta, + distance, + max, + victim->GetVID()); + ch->RecordAntiCheatViolation("MELEE_ANGLE", rotDelta > 140.0f ? 18 : 8, szDetail, rotDelta > 140.0f); + + if (test_server) + sys_log(0, "MELEE_ANGLE: %s rot_delta=%.1f distance=%d max=%d", ch->GetName(), rotDelta, distance, max); + + return BATTLE_NONE; + } } if (timed_event_cancel(ch)) @@ -176,6 +216,10 @@ int battle_melee_attack(LPCHARACTER ch, LPCHARACTER victim) int dam; int ret = battle_hit(ch, victim, dam); + + if ((ret == BATTLE_DAMAGE || ret == BATTLE_DEAD) && ch->IsPC() && victim->IsPC()) + victim->LockSyncOwner(450); + return (ret); } diff --git a/src/game/char.cpp b/src/game/char.cpp index fe4a686..dd1982a 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -189,6 +189,7 @@ void CHARACTER::Initialize() m_pkDestroyWhenIdleEvent = NULL; m_pkChrSyncOwner = NULL; + m_dwSyncOwnerLockExpire = 0; memset(&m_points, 0, sizeof(m_points)); memset(&m_pointsInstant, 0, sizeof(m_pointsInstant)); @@ -4317,6 +4318,7 @@ bool CHARACTER::SetSyncOwner(LPCHARACTER ch, bool bRemoveFromList) // 리스트에서 제거하지 않더라도 포인터는 NULL로 셋팅되어야 한다. m_pkChrSyncOwner = NULL; + m_dwSyncOwnerLockExpire = 0; } else { @@ -4345,6 +4347,7 @@ bool CHARACTER::SetSyncOwner(LPCHARACTER ch, bool bRemoveFromList) m_pkChrSyncOwner = ch; m_pkChrSyncOwner->m_kLst_pkChrSyncOwned.push_back(this); + m_dwSyncOwnerLockExpire = 0; // SyncOwner가 바뀌면 LastSyncTime을 초기화한다. static const timeval zero_tv = {0, 0}; @@ -4401,6 +4404,28 @@ bool CHARACTER::IsSyncOwner(LPCHARACTER ch) const return false; } +void CHARACTER::LockSyncOwner(DWORD dwDurationMs) +{ + if (!m_pkChrSyncOwner || !dwDurationMs) + return; + + const DWORD dwNow = get_dword_time(); + const DWORD dwExpire = dwNow + dwDurationMs; + + if (m_dwSyncOwnerLockExpire < dwExpire) + m_dwSyncOwnerLockExpire = dwExpire; +} + +bool CHARACTER::IsSyncOwnerLocked() const +{ + return m_pkChrSyncOwner && get_dword_time() < m_dwSyncOwnerLockExpire; +} + +bool CHARACTER::IsSyncOwnerLockedFor(LPCHARACTER ch) const +{ + return ch && m_pkChrSyncOwner == ch && get_dword_time() < m_dwSyncOwnerLockExpire; +} + void CHARACTER::SetParty(LPPARTY pkParty) { if (pkParty == m_pkParty) diff --git a/src/game/char.h b/src/game/char.h index 0c56987..b585f4d 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -829,6 +829,10 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider bool SetSyncOwner(LPCHARACTER ch, bool bRemoveFromList = true); bool IsSyncOwner(LPCHARACTER ch) const; + LPCHARACTER GetSyncOwner() const { return m_pkChrSyncOwner; } + void LockSyncOwner(DWORD dwDurationMs); + bool IsSyncOwnerLocked() const; + bool IsSyncOwnerLockedFor(LPCHARACTER ch) const; bool WarpSet(long x, long y, long lRealMapIndex = 0); void SetWarpLocation(long lMapIndex, long x, long y); @@ -852,6 +856,7 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider float m_fSyncTime; LPCHARACTER m_pkChrSyncOwner; + DWORD m_dwSyncOwnerLockExpire; CHARACTER_LIST m_kLst_pkChrSyncOwned; // 내가 SyncOwner인 자들 PIXEL_POSITION m_posDest; diff --git a/src/game/input_main.cpp b/src/game/input_main.cpp index b758d6d..20f3569 100644 --- a/src/game/input_main.cpp +++ b/src/game/input_main.cpp @@ -1580,12 +1580,35 @@ void CInputMain::Move(LPCHARACTER ch, const char * data) // FUNC_SKILL = 0x80, //}; + const float fDist = DISTANCE_SQRT((ch->GetX() - pinfo->lX) / 100, (ch->GetY() - pinfo->lY) / 100); + + if (ch->IsPC() && ch->IsSyncOwnerLocked() && ch->GetSyncOwner() && ch->GetSyncOwner() != ch && fDist > 2.5f) + { + char szDetail[192]; + snprintf(szDetail, + sizeof(szDetail), + "func=%u dist=%.1f owner=%s cur=(%ld,%ld) dst=(%ld,%ld)", + pinfo->bFunc, + fDist, + ch->GetSyncOwner()->GetName(), + static_cast(ch->GetX()), + static_cast(ch->GetY()), + static_cast(pinfo->lX), + static_cast(pinfo->lY)); + if (fDist > 8.0f) + ch->RecordAntiCheatViolation("SYNC_LOCK_MOVE", 8, szDetail, true); + else + sys_log(1, "SYNC_LOCK_MOVE: %s %s", ch->GetName(), szDetail); + + ch->Show(ch->GetMapIndex(), ch->GetX(), ch->GetY(), ch->GetZ()); + ch->Stop(); + return; + } + // 텔레포트 핵 체크 // if (!test_server) //2012.05.15 김용욱 : 테섭에서 (무적상태로) 다수 몬스터 상대로 다운되면서 공격시 콤보핵으로 죽는 문제가 있었다. { - const float fDist = DISTANCE_SQRT((ch->GetX() - pinfo->lX) / 100, (ch->GetY() - pinfo->lY) / 100); - if (((false == ch->IsRiding() && fDist > 25) || fDist > 40) && OXEVENT_MAP_INDEX != ch->GetMapIndex()) { char szDetail[160]; @@ -1916,6 +1939,21 @@ int CInputMain::SyncPosition(LPCHARACTER ch, const char * c_pcData, size_t uiByt continue; } + if (victim->IsSyncOwnerLocked() && !victim->IsSyncOwnerLockedFor(ch)) + { + char szDetail[192]; + snprintf(szDetail, + sizeof(szDetail), + "victim=%s owner=%s requester=%s sync=(%ld,%ld)", + victim->GetName(), + victim->GetSyncOwner() ? victim->GetSyncOwner()->GetName() : "-", + ch->GetName(), + static_cast(e->lX), + static_cast(e->lY)); + ch->RecordAntiCheatViolation("SYNC_OWNER_STEAL", 4, szDetail); + continue; + } + // 소유권 검사 if (!victim->SetSyncOwner(ch)) continue;