From 54adf8d32690ec4172b1492cc91a126adfd71e24 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Wed, 16 Sep 2026 00:24:35 -0400 Subject: [PATCH] Fix Timer units and make setInterval repeat The menu script passes intervals in milliseconds, but the Timer treated them as seconds, so wait_then_call(1100, open_mm) waited 1100 seconds and the menu never opened. setInterval also only fired once instead of repeating, so the button 'arrive' animation never advanced. Both are fixed, with a single scheduler thread serializing callbacks. --- .../quoije/umd/rco/vsmx/objects/Timer.java | 89 +++++++++++++++---- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java index 3868400..1e43ec2 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java @@ -11,17 +11,41 @@ import dev.quoije.umd.rco.vsmx.interpreter.VsmxNumber; import java.util.HashMap; import java.util.Map; -/** The Timer native object: setTimeout-style intervals for menu animations. */ +/** The Timer native object: recurring setInterval callbacks for menu animations. */ public class Timer extends BaseNativeObject { private final VsmxInterpreter interpreter; + private final Object callbackLock = new Object(); + private final Map timers = new HashMap<>(); private int currentTimerId; - private final Map timers = new HashMap<>(); + + /** One recurring timer task. */ + private static final class Task { + final VsmxBaseObject object; + final VsmxBaseObject function; + final VsmxBaseObject[] params; + final long intervalNanos; + long nextRunNanos; + boolean active = true; + + Task(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject[] params, long intervalNanos, long nextRunNanos) { + this.object = object; + this.function = function; + this.params = params; + this.intervalNanos = intervalNanos; + this.nextRunNanos = nextRunNanos; + } + } public static VsmxNativeObject create(VsmxInterpreter interpreter) { Timer timer = new Timer(interpreter); VsmxNativeObject object = new VsmxNativeObject(interpreter, timer); timer.setObject(object); + + Thread scheduler = new Thread(timer::run, "umd-timer"); + scheduler.setDaemon(true); + scheduler.start(); + return object; } @@ -29,24 +53,52 @@ public class Timer extends BaseNativeObject { this.interpreter = interpreter; } - private VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, - VsmxBaseObject... parameters) { - int id = currentTimerId++; - long millis = interval.getIntValue() * 1000L; - VsmxBaseObject[] params = parameters; - Thread thread = new Thread(() -> { + /** Single scheduler thread: fires due timers, serialized so the interpreter is never re-entered. */ + private void run() { + while (true) { + Task due = null; + long waitNanos = 50_000_000L; // 50ms default idle poll + synchronized (timers) { + long now = System.nanoTime(); + for (Task task : timers.values()) { + if (!task.active) { + continue; + } + if (task.nextRunNanos <= now) { + task.nextRunNanos = now + task.intervalNanos; + due = task; + break; + } + waitNanos = java.lang.Math.min(waitNanos, task.nextRunNanos - now); + } + } + + if (due != null) { + if (due.function instanceof VsmxFunction f) { + synchronized (callbackLock) { + interpreter.interpretFunction(f, due.object, due.params); + } + } + continue; + } + try { - Thread.sleep(millis); + Thread.sleep(waitNanos / 1_000_000L, (int) (waitNanos % 1_000_000L)); } catch (InterruptedException e) { return; } - if (function instanceof VsmxFunction f) { - interpreter.interpretFunction(f, object, params); - } - }, "umd-timer-" + id); - thread.setDaemon(true); - timers.put(id, thread); - thread.start(); + } + } + + private VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, + VsmxBaseObject... parameters) { + int id = currentTimerId++; + // The interval is expressed in milliseconds by the menu script. + long intervalNanos = java.lang.Math.max(1L, interval.getIntValue()) * 1_000_000L; + Task task = new Task(object, function, parameters, intervalNanos, System.nanoTime() + intervalNanos); + synchronized (timers) { + timers.put(id, task); + } VsmxArray result = new VsmxArray(interpreter, 1); result.setPropertyValue(0, new VsmxNumber(interpreter, id)); @@ -71,9 +123,8 @@ public class Timer extends BaseNativeObject { public void clearInterval(VsmxBaseObject object, VsmxBaseObject id) { int idValue = id.getPropertyValue(0).getIntValue(); - Thread thread = timers.remove(idValue); - if (thread != null) { - thread.interrupt(); + synchronized (timers) { + timers.remove(idValue); } } }