From 845ed4e527b874e944eb517ec137253a58c11d80 Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Thu, 2 Apr 2026 15:44:44 +0200 Subject: [PATCH] fix(assets): validate palette effect inputs and reset idle clock Reset PaletteEffectManager clock when adding the first effect after idle, preventing a huge delta spike from the accumulated gap. Add input validation to BTAPI palette effect methods: finite speed and integer range for paletteCycle, finite non-negative durationMs for paletteFade, paletteFadeRange, and paletteFlash. Co-Authored-By: Claude Signed-off-by: Vaclav Vancura --- src/assets/PaletteEffect.ts | 6 ++++++ src/core/BTAPI.ts | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/assets/PaletteEffect.ts b/src/assets/PaletteEffect.ts index 1671fc4..575a9d8 100644 --- a/src/assets/PaletteEffect.ts +++ b/src/assets/PaletteEffect.ts @@ -65,6 +65,12 @@ export class PaletteEffectManager { * @param effect - Effect instance to run each frame. */ add(effect: PaletteEffect): void { + // Reset the clock when waking from idle so the first update after a gap + // sees delta=0 instead of the entire idle duration. + if (this.effects.length === 0) { + this.lastTime = 0; + } + this.effects.push(effect); } diff --git a/src/core/BTAPI.ts b/src/core/BTAPI.ts index a2170d6..d25eb5e 100644 --- a/src/core/BTAPI.ts +++ b/src/core/BTAPI.ts @@ -561,6 +561,19 @@ export class BTAPI { // #region Palette Effects API + /** + * Validates that a duration is a finite, non-negative number. + * + * @param method - Calling method name for the error message. + * @param durationMs - Duration to validate. + * @throws Error if the duration is not finite or is negative. + */ + private assertFiniteDuration(method: string, durationMs: number): void { + if (!Number.isFinite(durationMs) || durationMs < 0) { + throw new Error(`[BT] ${method}: durationMs must be a finite non-negative number, got ${durationMs}.`); + } + } + /** * Starts rotating a range of palette entries at a constant speed. * @@ -572,6 +585,14 @@ export class BTAPI { * @param speed - Steps per second. Positive = forward, negative = backward. */ public paletteCycle(start: number, end: number, speed: number): void { + if (!Number.isFinite(speed)) { + throw new Error(`[BT] paletteCycle: speed must be finite, got ${speed}.`); + } + + if (!Number.isInteger(start) || !Number.isInteger(end) || start >= end) { + throw new Error(`[BT] paletteCycle: start must be an integer less than end, got [${start}, ${end}].`); + } + this.paletteEffects.add(new CycleEffect(start, end, speed)); } @@ -589,6 +610,7 @@ export class BTAPI { throw new Error('[BT] Cannot fade palette: no active palette set.'); } + this.assertFiniteDuration('paletteFade', durationMs); this.paletteEffects.add(new FadeEffect(this.palette, target, durationMs, easing)); } @@ -612,6 +634,7 @@ export class BTAPI { throw new Error('[BT] Cannot fade palette range: no active palette set.'); } + this.assertFiniteDuration('paletteFadeRange', durationMs); this.paletteEffects.add(new FadeRangeEffect(start, end, this.palette, target, durationMs, easing)); } @@ -624,6 +647,7 @@ export class BTAPI { * @param durationMs - How long the flash lasts in milliseconds. */ public paletteFlash(color: Color32, durationMs: number): void { + this.assertFiniteDuration('paletteFlash', durationMs); this.paletteEffects.add(new FlashEffect(color, durationMs)); } -- 2.51.2