From f79d5134c403575e1df123f907805e468819be3e Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 12:18:48 +0200 Subject: [PATCH] Harden fishing and mining action validation --- src/game/char.cpp | 127 ++++++++++++++++++++++++++++++++++++++++++++ src/game/char.h | 6 +++ src/game/mining.cpp | 39 +++++++++++++- 3 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/game/char.cpp b/src/game/char.cpp index 5ebf55d..a3f0538 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -140,6 +140,12 @@ void CHARACTER::Initialize() LastDropTime = 0; m_dwPickupWindowStart = 0; m_iPickupWindowCount = 0; + m_dwFishingWindowStart = 0; + m_iFishingWindowCount = 0; + m_lFishingStartX = 0; + m_lFishingStartY = 0; + m_dwMiningWindowStart = 0; + m_iMiningWindowCount = 0; m_iLastPMPulse = 0; m_iPMCounter = 0; @@ -4064,6 +4070,17 @@ void CHARACTER::ItemGetPacket(DWORD dwItemVnum, BYTE bCount, const char* szName, d->Packet(&pack, sizeof(pack)); } +namespace +{ + constexpr DWORD kActionWindowMs = 1000; + constexpr int kFishingRateWarnThreshold = 8; + constexpr int kFishingRateBlockThreshold = 18; + constexpr int kFishingTakeMaxMoveDistance = 1200; + constexpr int kMiningRateWarnThreshold = 6; + constexpr int kMiningRateBlockThreshold = 15; + constexpr int kMiningStartMaxDistance = 1000; +} + // MINING void CHARACTER::mining_take() { @@ -4082,18 +4099,72 @@ void CHARACTER::mining_cancel() void CHARACTER::mining(LPCHARACTER chLoad) { + const DWORD dwNow = get_dword_time(); + if (0 == m_dwMiningWindowStart || dwNow - m_dwMiningWindowStart >= kActionWindowMs) + { + m_dwMiningWindowStart = dwNow; + m_iMiningWindowCount = 0; + } + + ++m_iMiningWindowCount; + if (m_iMiningWindowCount > kMiningRateWarnThreshold) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "window=1s count=%d target=%u", m_iMiningWindowCount, chLoad ? chLoad->GetVID() : 0); + RecordAntiCheatViolation("MINING_RATE", MIN(8, 1 + (m_iMiningWindowCount - kMiningRateWarnThreshold) / 2), szDetail, m_iMiningWindowCount > kMiningRateBlockThreshold); + + if (m_iMiningWindowCount > kMiningRateBlockThreshold) + return; + } + if (m_pkMiningEvent) { mining_cancel(); return; } + if (IsDead() || !CanMove() || !CanHandleItem()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "dead=%d can_move=%d can_item=%d target=%u", + IsDead() ? 1 : 0, + CanMove() ? 1 : 0, + CanHandleItem() ? 1 : 0, + chLoad ? chLoad->GetVID() : 0); + RecordAntiCheatViolation("MINING_CONTEXT", 4, szDetail); + return; + } + if (!chLoad) return; if (mining::GetRawOreFromLoad(chLoad->GetRaceNum()) == 0) return; + if (chLoad == this || chLoad->GetMapIndex() != GetMapIndex()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "target=%u map=%ld target_map=%ld", + chLoad->GetVID(), + static_cast(GetMapIndex()), + static_cast(chLoad->GetMapIndex())); + RecordAntiCheatViolation("MINING_TARGET", 10, szDetail, true); + return; + } + + const int iDistance = DISTANCE_APPROX(GetX() - chLoad->GetX(), GetY() - chLoad->GetY()); + if (iDistance > kMiningStartMaxDistance) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "target=%u distance=%d max=%d", chLoad->GetVID(), iDistance, kMiningStartMaxDistance); + RecordAntiCheatViolation("MINING_TARGET", iDistance > kMiningStartMaxDistance + 800 ? 12 : 4, szDetail, iDistance > kMiningStartMaxDistance + 800); + return; + } + LPITEM pick = GetWear(WEAR_WEAPON); if (!pick || pick->GetType() != ITEM_PICK) @@ -4120,12 +4191,43 @@ void CHARACTER::mining(LPCHARACTER chLoad) void CHARACTER::fishing() { + const DWORD dwNow = get_dword_time(); + if (0 == m_dwFishingWindowStart || dwNow - m_dwFishingWindowStart >= kActionWindowMs) + { + m_dwFishingWindowStart = dwNow; + m_iFishingWindowCount = 0; + } + + ++m_iFishingWindowCount; + if (m_iFishingWindowCount > kFishingRateWarnThreshold) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "window=1s count=%d active=%d", m_iFishingWindowCount, m_pkFishingEvent ? 1 : 0); + RecordAntiCheatViolation("FISHING_RATE", MIN(8, 1 + (m_iFishingWindowCount - kFishingRateWarnThreshold) / 2), szDetail, m_iFishingWindowCount > kFishingRateBlockThreshold); + + if (m_iFishingWindowCount > kFishingRateBlockThreshold) + return; + } + if (m_pkFishingEvent) { fishing_take(); return; } + if (IsDead() || !CanMove() || !CanHandleItem()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "dead=%d can_move=%d can_item=%d", + IsDead() ? 1 : 0, + CanMove() ? 1 : 0, + CanHandleItem() ? 1 : 0); + RecordAntiCheatViolation("FISHING_CONTEXT", 4, szDetail); + return; + } + // 못감 속성에서 낚시를 시도한다? { LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(GetMapIndex()); @@ -4161,11 +4263,36 @@ void CHARACTER::fishing() float fx, fy; GetDeltaByDegree(GetRotation(), 400.0f, &fx, &fy); + m_lFishingStartX = GetX(); + m_lFishingStartY = GetY(); m_pkFishingEvent = fishing::CreateFishingEvent(this); } void CHARACTER::fishing_take() { + if (!CanMove() || !CanHandleItem()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "can_move=%d can_item=%d", + CanMove() ? 1 : 0, + CanHandleItem() ? 1 : 0); + RecordAntiCheatViolation("FISHING_CONTEXT", 4, szDetail); + event_cancel(&m_pkFishingEvent); + return; + } + + const int iMovedDistance = DISTANCE_APPROX(GetX() - m_lFishingStartX, GetY() - m_lFishingStartY); + if (iMovedDistance > kFishingTakeMaxMoveDistance) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "distance=%d max=%d", iMovedDistance, kFishingTakeMaxMoveDistance); + RecordAntiCheatViolation("FISHING_CONTEXT", iMovedDistance > kFishingTakeMaxMoveDistance + 800 ? 10 : 4, szDetail, iMovedDistance > kFishingTakeMaxMoveDistance + 800); + event_cancel(&m_pkFishingEvent); + return; + } + LPITEM rod = GetWear(WEAR_WEAPON); if (rod && rod->GetType() == ITEM_ROD) { diff --git a/src/game/char.h b/src/game/char.h index a87b161..68d6be7 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -2051,6 +2051,12 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider int CountDrops; DWORD m_dwPickupWindowStart; int m_iPickupWindowCount; + DWORD m_dwFishingWindowStart; + int m_iFishingWindowCount; + long m_lFishingStartX; + long m_lFishingStartY; + DWORD m_dwMiningWindowStart; + int m_iMiningWindowCount; void ClearPMCounter(void) { m_iPMCounter = 0; } void IncreasePMCounter(void) { m_iPMCounter++; } void SetLastPMPulse(void); diff --git a/src/game/mining.cpp b/src/game/mining.cpp index 8989baa..c8e8375 100644 --- a/src/game/mining.cpp +++ b/src/game/mining.cpp @@ -8,6 +8,7 @@ #include "db.h" #include "log.h" #include "skill.h" +#include "utils.h" namespace mining { @@ -16,6 +17,7 @@ namespace mining MAX_ORE = 18, MAX_FRACTION_COUNT = 9, ORE_COUNT_FOR_REFINE = 100, + MINING_COMPLETE_MAX_DISTANCE = 1200, }; struct SInfo @@ -366,6 +368,42 @@ namespace mining return 0; } + if (load->GetMapIndex() != ch->GetMapIndex()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "target=%u map=%ld target_map=%ld", + load->GetVID(), + static_cast(ch->GetMapIndex()), + static_cast(load->GetMapIndex())); + ch->RecordAntiCheatViolation("MINING_CONTEXT", 8, szDetail, true); + return 0; + } + + if (!ch->CanMove() || !ch->CanHandleItem()) + { + char szDetail[128]; + snprintf(szDetail, + sizeof(szDetail), + "can_move=%d can_item=%d target=%u", + ch->CanMove() ? 1 : 0, + ch->CanHandleItem() ? 1 : 0, + load->GetVID()); + ch->RecordAntiCheatViolation("MINING_CONTEXT", 4, szDetail); + return 0; + } + + const int iDistance = DISTANCE_APPROX(ch->GetX() - load->GetX(), ch->GetY() - load->GetY()); + if (iDistance > MINING_COMPLETE_MAX_DISTANCE) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "target=%u distance=%d max=%d", load->GetVID(), iDistance, MINING_COMPLETE_MAX_DISTANCE); + ch->RecordAntiCheatViolation("MINING_DISTANCE", iDistance > MINING_COMPLETE_MAX_DISTANCE + 800 ? 12 : 4, szDetail, iDistance > MINING_COMPLETE_MAX_DISTANCE + 800); + ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("너무 멀리 떨어져 있어 채광을 완료할 수 없습니다.")); + return 0; + } + int iPct = GetOrePct(ch); if (number(1, 100) <= iPct) @@ -445,4 +483,3 @@ namespace mining return false; } } -