Skip to content

Commit 30081b5

Browse files
Cheater_Detection: fixes to the round robin algoritm for doubletap detection
1 parent d57c82d commit 30081b5

2 files changed

Lines changed: 171 additions & 15 deletions

File tree

Cheater_Detection/Main.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ local function OnCreateMove(cmd)
500500

501501
if enableDoubleTap then
502502
Profiler.Begin("DoubleTap")
503+
DoubleTap.BeginDetectionTick(activePlayers, curTick)
503504
for _, pState in ipairs(activePlayers) do
504505
if DoubleTap.HasWork(pState) then
505506
DoubleTap.ProcessPlayer(pState)

Cheater_Detection/detectors/double_tap.lua

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ local DT_EVIDENCE_COOLDOWN_S = 0.5
3737
local REJECT_LOG_INTERVAL_S = 0.35
3838
local HURT_LOG_INTERVAL_S = 1.0
3939
local WATCH_BURST_SCAN_INTERVAL = 4 -- ticks between scans while waiting for post-hit burst
40+
local MAX_HURT_BURST_SCANS_TICK = 10 -- routine cap per game tick (busy fights)
41+
local MAX_HURT_BURST_SCANS_HOT = 14 -- absolute cap; hot shooters may exceed routine budget
42+
43+
local DT_FOCUS_PRIORITY_WATCH = 3 -- post-hit: waiting for simtime burst
44+
local DT_FOCUS_PRIORITY_BURST = 2 -- burst seen: waiting for hurt
45+
local DT_FOCUS_PRIORITY_SUSPECT = 1 -- already has double_tap evidence
46+
local DT_FOCUS_PRIORITY_ROUTINE = 0 -- under cap, background scan
4047

4148
local cachedTickInterval = nil
4249
local cachedBurstMinTicks = nil
@@ -49,6 +56,15 @@ local burstThisTick = {}
4956
local lastBurstCleanTick = 0
5057
local lastServerHitchTick = -math.huge
5158

59+
-- One burst-scan ProcessPlayer per game tick; hot suspects preempt the round-robin pool.
60+
local focusTick = -1
61+
local focusedPlayerId = nil
62+
local hotRoundRobinIndex = 0
63+
local warmRoundRobinIndex = 0
64+
local coldRoundRobinIndex = 0
65+
local hurtBurstScanTick = -1
66+
local hurtBurstScansUsed = 0
67+
5268
local function getBurstThresholds()
5369
local tickInt = globals.TickInterval()
5470
if tickInt ~= cachedTickInterval then
@@ -176,6 +192,7 @@ local function getState(id)
176192
lastRejectTag = nil,
177193
lastRejectLogTime = 0,
178194
lastHurtLogTime = 0,
195+
lastHurtBurstScanTick = 0,
179196
}
180197
end
181198
return playerState[id]
@@ -227,6 +244,129 @@ local function isWatchingForBurst(id)
227244
return elapsed >= 0 and elapsed <= getConfirmWindowTicks()
228245
end
229246

247+
local function getDtEvidenceWeight(id)
248+
local weight = Evidence.GetMethodWeight(id, "double_tap")
249+
if weight <= 0 then
250+
weight = Evidence.GetMethodWeight(id, "warp_dt")
251+
end
252+
return weight
253+
end
254+
255+
local function getDtEvidenceCap()
256+
local cap = Evidence.GetMethodScoreCap("double_tap")
257+
if cap <= 0 then
258+
cap = Evidence.GetMethodScoreCap("warp_dt")
259+
end
260+
return cap
261+
end
262+
263+
local function getDtFocusPriority(id)
264+
if not id or id:sub(1, 4) == "BOT_" then
265+
return -1
266+
end
267+
if isWatchingForBurst(id) then
268+
return DT_FOCUS_PRIORITY_WATCH
269+
end
270+
local data = getState(id)
271+
if data.lastBurstTick > 0 then
272+
return DT_FOCUS_PRIORITY_BURST
273+
end
274+
local weight = getDtEvidenceWeight(id)
275+
local cap = getDtEvidenceCap()
276+
if weight >= cap then
277+
return -1
278+
end
279+
if weight > 0 then
280+
return DT_FOCUS_PRIORITY_SUSPECT
281+
end
282+
return DT_FOCUS_PRIORITY_ROUTINE
283+
end
284+
285+
local function pickRoundRobinId(pool, indexField)
286+
if #pool == 0 then
287+
return nil, indexField
288+
end
289+
local nextIndex = (indexField % #pool) + 1
290+
return pool[nextIndex], nextIndex
291+
end
292+
293+
-- Among simultaneous hot shooters, prefer whoever hurt most recently; RR on ties.
294+
local function pickHotPoolFocus(pool, rrIndex)
295+
local bestDamageTick = -1
296+
for i = 1, #pool do
297+
local damageTick = getState(pool[i]).lastDamageTick
298+
if damageTick > bestDamageTick then
299+
bestDamageTick = damageTick
300+
end
301+
end
302+
local tied = {}
303+
for i = 1, #pool do
304+
if getState(pool[i]).lastDamageTick == bestDamageTick then
305+
tied[#tied + 1] = pool[i]
306+
end
307+
end
308+
if #tied == 0 then
309+
return pickRoundRobinId(pool, rrIndex)
310+
end
311+
if #tied == 1 then
312+
return tied[1], rrIndex
313+
end
314+
return pickRoundRobinId(tied, rrIndex)
315+
end
316+
317+
local function resetHurtBurstScanBudget(curTick)
318+
if hurtBurstScanTick ~= curTick then
319+
hurtBurstScanTick = curTick
320+
hurtBurstScansUsed = 0
321+
end
322+
end
323+
324+
function DoubleTap.BeginDetectionTick(activePlayers, curTick)
325+
if not isEnabled() or focusTick == curTick then
326+
return
327+
end
328+
focusTick = curTick
329+
focusedPlayerId = nil
330+
resetHurtBurstScanBudget(curTick)
331+
332+
if not activePlayers or #activePlayers == 0 then
333+
return
334+
end
335+
336+
local hotPool, warmPool, coldPool = {}, {}, {}
337+
for i = 1, #activePlayers do
338+
local pState = activePlayers[i]
339+
local id = pState and pState.id
340+
if not id then
341+
goto continue
342+
end
343+
local priority = getDtFocusPriority(id)
344+
if priority == DT_FOCUS_PRIORITY_WATCH or priority == DT_FOCUS_PRIORITY_BURST then
345+
hotPool[#hotPool + 1] = id
346+
elseif priority == DT_FOCUS_PRIORITY_SUSPECT then
347+
warmPool[#warmPool + 1] = id
348+
elseif priority == DT_FOCUS_PRIORITY_ROUTINE then
349+
coldPool[#coldPool + 1] = id
350+
end
351+
::continue::
352+
end
353+
354+
local chosenId
355+
if #hotPool > 0 then
356+
chosenId, hotRoundRobinIndex = pickHotPoolFocus(hotPool, hotRoundRobinIndex)
357+
elseif #warmPool > 0 then
358+
chosenId, warmRoundRobinIndex = pickRoundRobinId(warmPool, warmRoundRobinIndex)
359+
elseif #coldPool > 0 then
360+
chosenId, coldRoundRobinIndex = pickRoundRobinId(coldPool, coldRoundRobinIndex)
361+
end
362+
363+
focusedPlayerId = chosenId
364+
end
365+
366+
local function isFocusedPlayer(id)
367+
return focusedPlayerId ~= nil and id == focusedPlayerId
368+
end
369+
230370
local function clearStaleBurstState(data, curTick)
231371
if data.lastBurstTick <= 0 then
232372
return
@@ -413,6 +553,9 @@ local function onBurstDetected(id, burstAmount, curTick, burstIndex)
413553
end
414554

415555
local function shouldScanBurstThisTick(id, curTick)
556+
if isFocusedPlayer(id) and getDtFocusPriority(id) >= DT_FOCUS_PRIORITY_BURST then
557+
return true
558+
end
416559
if not isWatchingForBurst(id) then
417560
return true
418561
end
@@ -451,6 +594,25 @@ local function tryDetectBurstForPlayer(id, curTick, forceScan)
451594
onBurstDetected(id, burstAmount, curTick, burstIndex)
452595
end
453596

597+
-- Hurt path: correlate every hit; burst scan at most once per attacker per tick (+ global cap).
598+
local function tryHurtBurstScan(id, curTick)
599+
resetHurtBurstScanBudget(curTick)
600+
local data = getState(id)
601+
if data.lastHurtBurstScanTick == curTick then
602+
return
603+
end
604+
local scanCap = MAX_HURT_BURST_SCANS_TICK
605+
if getDtFocusPriority(id) >= DT_FOCUS_PRIORITY_WATCH then
606+
scanCap = MAX_HURT_BURST_SCANS_HOT
607+
end
608+
if hurtBurstScansUsed >= scanCap then
609+
return
610+
end
611+
data.lastHurtBurstScanTick = curTick
612+
hurtBurstScansUsed = hurtBurstScansUsed + 1
613+
tryDetectBurstForPlayer(id, curTick, true)
614+
end
615+
454616
function DoubleTap.HasWork(playerState)
455617
if not isEnabled() then
456618
return false
@@ -459,21 +621,10 @@ function DoubleTap.HasWork(playerState)
459621
return false
460622
end
461623
local id = playerState.id
462-
local weight = Evidence.GetMethodWeight(id, "double_tap")
463-
if weight <= 0 then
464-
weight = Evidence.GetMethodWeight(id, "warp_dt")
465-
end
466-
local cap = Evidence.GetMethodScoreCap("double_tap")
467-
if cap <= 0 then
468-
cap = Evidence.GetMethodScoreCap("warp_dt")
469-
end
470-
if weight < cap then
471-
return true
472-
end
473-
if isWatchingForBurst(id) then
474-
return (globals.TickCount() % WATCH_BURST_SCAN_INTERVAL) == 0
624+
if getDtFocusPriority(id) < 0 then
625+
return false
475626
end
476-
return (globals.TickCount() % 4) == 0
627+
return isFocusedPlayer(id)
477628
end
478629

479630
function DoubleTap.ProcessPlayer(playerState)
@@ -511,6 +662,7 @@ function DoubleTap.ProcessPlayer(playerState)
511662
end
512663

513664
function DoubleTap.Tick()
665+
-- Focus is chosen in BeginDetectionTick (Main.lua) once per game tick.
514666
end
515667

516668
Events.Subscribe("OnHitscanHit", function(hit)
@@ -543,7 +695,7 @@ Events.Subscribe("OnHitscanHit", function(hit)
543695
end
544696

545697
data.lastDamageTick = curTick
546-
tryDetectBurstForPlayer(attackerID, curTick, true)
698+
tryHurtBurstScan(attackerID, curTick)
547699

548700
if isDtLogEnabled() then
549701
local now = globals.RealTime()
@@ -559,6 +711,9 @@ end)
559711
local function onPlayerGone(id)
560712
playerState[id] = nil
561713
dtSuppressUntil[id] = nil
714+
if focusedPlayerId == id then
715+
focusedPlayerId = nil
716+
end
562717
end
563718

564719
Events.Subscribe("OnPlayerDisconnect", onPlayerGone)

0 commit comments

Comments
 (0)