From ba776fc03f9a0bc671a3ef18018a2f3b38a21b5e Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Sat, 9 May 2026 09:23:04 +0200 Subject: [PATCH] fix(utils): prevent timer lock on hard tick-counter rewind When lastFiredTick is well ahead of currentTick (e.g. after a global reset to 0), elapsedTicks() clamped to 0 but the old baseline was never cleared. The timer would lock until currentTick reached lastFiredTick + intervalTicks. Add an explicit guard at the top of tick(): if currentTick < lastFiredTick, reset the baseline to currentTick and return false. The timer then fires again after exactly intervalTicks from the rewound position. Splits the shallow-rewind test and adds a dedicated hard-rewind regression test (lastFiredTick=1000, rewind to 0). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Vaclav Vancura --- src/utils/Timer.test.ts | 17 ++++++++++++++++- src/utils/Timer.ts | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/utils/Timer.test.ts b/src/utils/Timer.test.ts index a60790d..6d71a9c 100644 --- a/src/utils/Timer.test.ts +++ b/src/utils/Timer.test.ts @@ -57,7 +57,7 @@ describe('Timer', () => { expect(timer.tick()).toBe(true); }); - it('does not lock when current tick rewinds', () => { + it('does not lock when current tick rewinds shallowly', () => { vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(0); const timer = new Timer(5); @@ -66,6 +66,21 @@ describe('Timer', () => { expect(timer.remainingTicks(3)).toBe(5); expect(timer.tick(10)).toBe(true); }); + + it('resets baseline and does not lock after a hard tick rewind', () => { + vi.spyOn(BTAPI.instance, 'getTicks').mockReturnValue(0); + const timer = new Timer(5); + + // Advance well past the interval so lastFiredTick is 1000. + expect(timer.tick(1000)).toBe(true); + + // Hard rewind to 0 - without the guard the timer would lock until tick 1005. + expect(timer.tick(0)).toBe(false); // rewind detected, baseline reset to 0 + + // Should now fire exactly intervalTicks later. + expect(timer.tick(4)).toBe(false); + expect(timer.tick(5)).toBe(true); + }); }); describe('reset and helpers', () => { diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index a0c081f..e55a8ee 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -34,6 +34,11 @@ export class Timer { * @returns True when at least `intervalTicks` have elapsed since the last fire/reset. */ public tick(currentTick: number = BTAPI.instance.getTicks()): boolean { + if (currentTick < this.lastFiredTick) { + this.lastFiredTick = currentTick; + return false; + } + if (this.elapsedTicks(currentTick) < this.intervalTicks) { return false; } -- 2.51.2