@@ -63,6 +63,7 @@ local FPS_ROLLING_WINDOW_S = 1.0 -- 1s
6363local DEBUG_SUMMARY_INTERVAL_S = 1.0
6464local DEBUG_DELTA_WINDOW = 16 -- newest simtime gaps to classify for Choke logs
6565local DEBUG_SAMPLE_TICK_INTERVAL = 4 -- Choke log rollup: full window snapshot every N ticks
66+ local FL_PLAYER_PHASE_COUNT = 4 -- stagger players across ticks (was: all on tick%4)
6667-- Newest simtime gaps to read; clamped to DetectionConfig simtime retention (see cd_simhistory).
6768local FAKE_LAG_EVIDENCE_CAP = Evidence .GetMethodScoreCap (" fake_lag" ) -- suspicious-only tuning cap
6869-- Below ~40 fps, frame gaps can exceed the 8-tick choke threshold (tickrate/fps). No listen/debug bypass.
@@ -75,6 +76,14 @@ local debugSummaries = {}
7576local smoothedFrameTime = 1 / 60
7677local rollingFrameSamples = {} -- { t, ft } within FPS_ROLLING_WINDOW_S
7778local _positiveDeltaBuf = {}
79+ local cachedGapMaxTicks = 23
80+ local flTickCache = {
81+ curTick = - 1 ,
82+ blockReason = nil ,
83+ flScan = 20 ,
84+ isChokeDebug = false ,
85+ playerPhase = 0 ,
86+ }
7887
7988-- Pre-HistoryManager, delta lists only had positive simtime steps (stalls omitted).
8089-- Scoring thresholds assume that shape; full buffer (with 0t holds) is for stall/burst paths only.
@@ -232,41 +241,56 @@ function FakeLag.IsDetectionAllowed()
232241 return FakeLag .GetDetectionBlockReason () == nil
233242end
234243
244+ --- Once per game tick before the player loop (Main.lua). Caches FPS gate, scan depth, gap cap.
245+ function FakeLag .BeginDetectionTick (curTick )
246+ if flTickCache .curTick == curTick then
247+ return
248+ end
249+ flTickCache .curTick = curTick
250+ flTickCache .playerPhase = curTick % FL_PLAYER_PHASE_COUNT
251+ flTickCache .isChokeDebug = Common .IsLogCategoryEnabled (" Choke" )
252+ flTickCache .flScan = DetectionConfig .GetSimtimeScanLimits ().flScan
253+ cachedGapMaxTicks = DoubleTap .GetFakeLagMaxChokeTicks () + 2
254+ flTickCache .blockReason = FakeLag .GetDetectionBlockReason ()
255+ end
256+
235257function FakeLag .HasWork (playerState )
236258 if not playerState or not playerState .id then
237259 return false
238260 end
239- if not FakeLag .IsDetectionAllowed () then
261+ local curTick = globals .TickCount ()
262+ if flTickCache .curTick ~= curTick then
263+ FakeLag .BeginDetectionTick (curTick )
264+ end
265+ if flTickCache .blockReason then
240266 return false
241267 end
268+
269+ local entIndex = playerState .entityIndex or 0
270+ if (entIndex % FL_PLAYER_PHASE_COUNT ) ~= flTickCache .playerPhase then
271+ return false
272+ end
273+
242274 if (playerState .flags & Constants .Flags .CHEATER ) ~= 0 then
243- return Common . IsLogCategoryEnabled ( " Choke " )
275+ return flTickCache . isChokeDebug
244276 end
277+
245278 local id = playerState .id
246- if Common .IsLogCategoryEnabled (" Choke" ) then
247- if Evidence .GetMethodWeight (id , " fake_lag" ) >= FAKE_LAG_EVIDENCE_CAP then
248- return false
249- end
250- return true
251- end
252279 if Evidence .GetMethodWeight (id , " fake_lag" ) >= FAKE_LAG_EVIDENCE_CAP then
253- return false
280+ return flTickCache . isChokeDebug
254281 end
255- return (globals .TickCount () % 4 ) == 0
282+
283+ return true
256284end
257285
258286local function getRhythmTolerance (anchorDelta )
259287 return math.max (RHYTHM_TOLERANCE_TICKS , math.floor (anchorDelta * 0.2 + 0.5 ))
260288end
261289
262- local function getFlGapMaxTicks ()
263- return DoubleTap .GetFakeLagMaxChokeTicks () + 2
264- end
265-
266290local function isFlSizedGap (tickDelta )
267291 return tickDelta
268292 and tickDelta >= MIN_FAKELAG_CHOKE_TICKS
269- and tickDelta <= getFlGapMaxTicks ()
293+ and tickDelta <= cachedGapMaxTicks
270294end
271295
272296local function countStallsInWindow (deltaTicks , sampleCount , deltaCount )
@@ -611,13 +635,11 @@ local function addCappedFakeLagEvidence(id, wantedWeight)
611635 return 0
612636end
613637
614- local function isActiveChokePattern (deltaTicks , deltaCount )
638+ local function isActiveChokePattern (deltaTicks , deltaCount , posDeltas , posCount )
615639 if getChokeHoldSignal (deltaTicks , RECENT_CHOKE_SAMPLE_COUNT , deltaCount ) then
616640 return true
617641 end
618642
619- local posDeltas , posCount = buildPositiveDeltaView (deltaTicks , deltaCount )
620-
621643 if getSustainedChokeAverage (posDeltas , posCount ) then
622644 return true
623645 end
@@ -670,8 +692,11 @@ function FakeLag.ProcessPlayer(playerState)
670692 return
671693 end
672694
673- local blockReason = FakeLag .GetDetectionBlockReason ()
674- if blockReason then
695+ local curTick = globals .TickCount ()
696+ if flTickCache .curTick ~= curTick then
697+ FakeLag .BeginDetectionTick (curTick )
698+ end
699+ if flTickCache .blockReason then
675700 return
676701 end
677702
@@ -688,7 +713,7 @@ function FakeLag.ProcessPlayer(playerState)
688713 if history ._count < 5 then return end
689714
690715 local now = globals .RealTime ()
691- local isDebug = Common . IsLogCategoryEnabled ( " Choke " )
716+ local isDebug = flTickCache . isChokeDebug
692717
693718 if not isDebug then
694719 if Evidence .GetMethodWeight (id , " fake_lag" ) >= FAKE_LAG_EVIDENCE_CAP then
@@ -701,8 +726,7 @@ function FakeLag.ProcessPlayer(playerState)
701726 end
702727 end
703728
704- local flScan = DetectionConfig .GetSimtimeScanLimits ().flScan
705- local deltaTicks , deltaCount , _sumTicks = HistoryManager .GetSimDeltaDeltas (history , flScan )
729+ local deltaTicks , deltaCount , _sumTicks = HistoryManager .GetSimDeltaDeltas (history , flTickCache .flScan )
706730 if deltaCount < 1 then
707731 return
708732 end
@@ -715,7 +739,7 @@ function FakeLag.ProcessPlayer(playerState)
715739 end
716740
717741 -- Ongoing choke: pause exploit decay so score does not drain between evidence cooldowns.
718- if isActiveChokePattern (deltaTicks , deltaCount ) then
742+ if isActiveChokePattern (deltaTicks , deltaCount , posDeltas , posCount ) then
719743 Evidence .HoldDecayForMethod (id , " fake_lag" )
720744 end
721745
0 commit comments