diff --git a/src/game/char.cpp b/src/game/char.cpp index 94e124e..93fd167 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -140,6 +140,15 @@ void CHARACTER::Initialize() LastDropTime = 0; m_dwPickupWindowStart = 0; m_iPickupWindowCount = 0; + m_dwPickupPatternWindowStart = 0; + m_dwLastPickupPatternTime = 0; + m_lLastPickupPatternX = 0; + m_lLastPickupPatternY = 0; + m_iPickupPatternSampleCount = 0; + m_iPickupPatternMinIntervalMs = 0; + m_iPickupPatternMaxIntervalMs = 0; + m_iPickupPatternMinStep = 0; + m_iPickupPatternMaxStep = 0; m_dwFishingWindowStart = 0; m_iFishingWindowCount = 0; m_lFishingStartX = 0; diff --git a/src/game/char.h b/src/game/char.h index c4f143f..9a7a913 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -2052,6 +2052,15 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider int CountDrops; DWORD m_dwPickupWindowStart; int m_iPickupWindowCount; + DWORD m_dwPickupPatternWindowStart; + DWORD m_dwLastPickupPatternTime; + long m_lLastPickupPatternX; + long m_lLastPickupPatternY; + int m_iPickupPatternSampleCount; + int m_iPickupPatternMinIntervalMs; + int m_iPickupPatternMaxIntervalMs; + int m_iPickupPatternMinStep; + int m_iPickupPatternMaxStep; DWORD m_dwFishingWindowStart; int m_iFishingWindowCount; long m_lFishingStartX; diff --git a/src/game/char_item.cpp b/src/game/char_item.cpp index 236eacc..4e18ff9 100644 --- a/src/game/char_item.cpp +++ b/src/game/char_item.cpp @@ -46,6 +46,12 @@ namespace { constexpr int kRefineNpcMaxDistance = 2000; + constexpr DWORD kPickupPatternWindowMs = 8 * 60 * 1000; + constexpr int kPickupPatternMinSamples = 12; + constexpr int kPickupPatternMaxIntervalSpreadMs = 180; + constexpr int kPickupPatternMaxStepSpread = 220; + constexpr int kPickupPatternMinRouteStep = 250; + constexpr int kPickupPatternMaxIntervalMs = 15000; bool ValidateStoredRefineNpc(LPCHARACTER ch, DWORD dwRefineNpcVID, const char* action) { @@ -92,6 +98,84 @@ namespace return true; } + + void ResetPickupPatternWindow(LPCHARACTER ch, DWORD now, long x, long y) + { + ch->m_dwPickupPatternWindowStart = now; + ch->m_dwLastPickupPatternTime = now; + ch->m_lLastPickupPatternX = x; + ch->m_lLastPickupPatternY = y; + ch->m_iPickupPatternSampleCount = 0; + ch->m_iPickupPatternMinIntervalMs = 0; + ch->m_iPickupPatternMaxIntervalMs = 0; + ch->m_iPickupPatternMinStep = 0; + ch->m_iPickupPatternMaxStep = 0; + } + + void ObservePickupPattern(LPCHARACTER ch, DWORD now, long x, long y) + { + if (!ch) + return; + + if (0 == ch->m_dwPickupPatternWindowStart || now - ch->m_dwPickupPatternWindowStart >= kPickupPatternWindowMs) + { + ResetPickupPatternWindow(ch, now, x, y); + return; + } + + const int iIntervalMs = static_cast(now - ch->m_dwLastPickupPatternTime); + const int iStep = DISTANCE_APPROX(x - ch->m_lLastPickupPatternX, y - ch->m_lLastPickupPatternY); + + ch->m_dwLastPickupPatternTime = now; + ch->m_lLastPickupPatternX = x; + ch->m_lLastPickupPatternY = y; + + if (iIntervalMs <= 0 || iIntervalMs > kPickupPatternMaxIntervalMs) + { + ResetPickupPatternWindow(ch, now, x, y); + return; + } + + if (0 == ch->m_iPickupPatternSampleCount) + { + ch->m_iPickupPatternSampleCount = 1; + ch->m_iPickupPatternMinIntervalMs = iIntervalMs; + ch->m_iPickupPatternMaxIntervalMs = iIntervalMs; + ch->m_iPickupPatternMinStep = iStep; + ch->m_iPickupPatternMaxStep = iStep; + return; + } + + ++ch->m_iPickupPatternSampleCount; + ch->m_iPickupPatternMinIntervalMs = MIN(ch->m_iPickupPatternMinIntervalMs, iIntervalMs); + ch->m_iPickupPatternMaxIntervalMs = MAX(ch->m_iPickupPatternMaxIntervalMs, iIntervalMs); + ch->m_iPickupPatternMinStep = MIN(ch->m_iPickupPatternMinStep, iStep); + ch->m_iPickupPatternMaxStep = MAX(ch->m_iPickupPatternMaxStep, iStep); + + if (ch->m_iPickupPatternSampleCount < kPickupPatternMinSamples) + return; + + const int iIntervalSpread = ch->m_iPickupPatternMaxIntervalMs - ch->m_iPickupPatternMinIntervalMs; + const int iStepSpread = ch->m_iPickupPatternMaxStep - ch->m_iPickupPatternMinStep; + if (iIntervalSpread > kPickupPatternMaxIntervalSpreadMs || iStepSpread > kPickupPatternMaxStepSpread || ch->m_iPickupPatternMaxStep < kPickupPatternMinRouteStep) + return; + + char szDetail[160]; + snprintf(szDetail, + sizeof(szDetail), + "samples=%d interval=%d..%d step=%d..%d", + ch->m_iPickupPatternSampleCount, + ch->m_iPickupPatternMinIntervalMs, + ch->m_iPickupPatternMaxIntervalMs, + ch->m_iPickupPatternMinStep, + ch->m_iPickupPatternMaxStep); + ch->RecordAntiCheatViolation("PICKUP_PATTERN", + iIntervalSpread <= 100 && iStepSpread <= 140 ? 4 : 2, + szDetail, + iIntervalSpread <= 100 && iStepSpread <= 140); + + ResetPickupPatternWindow(ch, now, x, y); + } } const int ITEM_BROKEN_METIN_VNUM = 28960; @@ -5908,6 +5992,8 @@ bool CHARACTER::PickupItem(DWORD dwVID) return false; const DWORD dwNow = get_dword_time(); + const long lPickupX = GetX(); + const long lPickupY = GetY(); if (m_dwPickupWindowStart == 0 || dwNow - m_dwPickupWindowStart >= 1000) { @@ -5990,6 +6076,7 @@ bool CHARACTER::PickupItem(DWORD dwVID) if (bCount == 0) { ItemGetPacket(item2->GetVnum(), item2->GetCount()); + ObservePickupPattern(this, dwNow, lPickupX, lPickupY); M2_DESTROY_ITEM(item); if (item2->GetType() == ITEM_QUEST) quest::CQuestManager::instance().PickupItem (GetPlayerID(), item2); @@ -6038,6 +6125,7 @@ bool CHARACTER::PickupItem(DWORD dwVID) } //Motion(MOTION_PICKUP); + ObservePickupPattern(this, dwNow, lPickupX, lPickupY); return true; } else if (item->HasOwnership()) @@ -6110,6 +6198,7 @@ bool CHARACTER::PickupItem(DWORD dwVID) if (item->GetType() == ITEM_QUEST) quest::CQuestManager::instance().PickupItem (owner->GetPlayerID(), item); + ObservePickupPattern(this, dwNow, lPickupX, lPickupY); return true; }