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.
This commit is contained in:
2026-09-16 00:24:35 -04:00
parent 4d66cc73db
commit 54adf8d326
@@ -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<Integer, Task> timers = new HashMap<>();
private int currentTimerId;
private final Map<Integer, Thread> 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);
}
}
}