Fix Scenes freeze and Languages up/down navigation

- VsmxInterpreter.interpretFunction: save/restore pc/exit/stack/callStates so
  re-entrant event firing (FireEventAnim, timers, movie callbacks) no longer
  truncates the outer script. This is what left ss_btn_mm without focus
  (Scenes appeared frozen: no selection, input ignored while audio continued).
- Add default geometric focus traversal for buttons with no direction handler:
  Controller.moveFocus picks the nearest visible focusable object. The PSP does
  this natively; English/Francais buttons have no onUp/onDown events in the RCO
  and relied on it.
- Track page visibility (BaseObject.displayed, set by PageObject.open/close) so
  traversal skips closed pages and collapsed (alpha 0) columns.
- Make BaseObject.trigger null-safe.
This commit is contained in:
2026-09-17 18:48:24 -04:00
parent 298de29e5f
commit 8fa8dc4ba9
6 changed files with 157 additions and 9 deletions
@@ -30,6 +30,7 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay {
protected Controller controller; protected Controller controller;
private String name; private String name;
private BaseNativeObject parent; private BaseNativeObject parent;
private boolean displayed;
private Field[] getSortedFields() { private Field[] getSortedFields() {
Field[] fields = getClass().getFields(); Field[] fields = getClass().getFields();
@@ -140,8 +141,17 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay {
return name; return name;
} }
/** True while this object is part of an opened page's display list. */
public boolean isDisplayed() {
return displayed;
}
public void setDisplayed(boolean displayed) {
this.displayed = displayed;
}
protected void trigger(EventType event) { protected void trigger(EventType event) {
if (event.getEvent() != null && controller != null) { if (event != null && event.getEvent() != null && controller != null) {
controller.getInterpreter().interpretScript(getObject(), event.getEvent()); controller.getInterpreter().interpretScript(getObject(), event.getEvent());
} }
} }
@@ -7,6 +7,7 @@ import dev.quoije.umd.rco.type.ImageType;
import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.TextType; import dev.quoije.umd.rco.type.TextType;
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject; import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
import dev.quoije.umd.rco.vsmx.objects.Controller;
/** A button object. */ /** A button object. */
public class ButtonObject extends BasePositionObject { public class ButtonObject extends BasePositionObject {
@@ -49,6 +50,14 @@ public class ButtonObject extends BasePositionObject {
return focus.getImage(); return focus.getImage();
} }
@Override
public void setController(Controller controller) {
super.setController(controller);
if (controller != null) {
controller.registerFocusable(this);
}
}
@Override @Override
public void setFocus() { public void setFocus() {
trigger(onFocusIn); trigger(onFocusIn);
@@ -68,21 +77,37 @@ public class ButtonObject extends BasePositionObject {
@Override @Override
public void onUp() { public void onUp() {
trigger(onFocusUp); if (onFocusUp != null && onFocusUp.getEvent() != null) {
trigger(onFocusUp);
} else if (controller != null) {
controller.moveFocus(this, 0, 1);
}
} }
@Override @Override
public void onDown() { public void onDown() {
trigger(onFocusDown); if (onFocusDown != null && onFocusDown.getEvent() != null) {
trigger(onFocusDown);
} else if (controller != null) {
controller.moveFocus(this, 0, -1);
}
} }
@Override @Override
public void onLeft() { public void onLeft() {
trigger(onFocusLeft); if (onFocusLeft != null && onFocusLeft.getEvent() != null) {
trigger(onFocusLeft);
} else if (controller != null) {
controller.moveFocus(this, -1, 0);
}
} }
@Override @Override
public void onRight() { public void onRight() {
trigger(onFocusRight); if (onFocusRight != null && onFocusRight.getEvent() != null) {
trigger(onFocusRight);
} else if (controller != null) {
controller.moveFocus(this, 1, 0);
}
} }
} }
@@ -35,6 +35,7 @@ public class PageObject extends BaseObject {
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName); VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
for (String childName : children.getPropertyNames()) { for (String childName : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(childName); VsmxBaseObject child = children.getPropertyValue(childName);
setDisplayed(child, true);
display.add(child); display.add(child);
display(child); display(child);
} }
@@ -47,11 +48,18 @@ public class PageObject extends BaseObject {
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName); VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
for (String childName : children.getPropertyNames()) { for (String childName : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(childName); VsmxBaseObject child = children.getPropertyValue(childName);
setDisplayed(child, false);
display.remove(child); display.remove(child);
hide(child); hide(child);
} }
} }
private static void setDisplayed(VsmxBaseObject object, boolean displayed) {
if (object instanceof VsmxNativeObject nativeObject && nativeObject.getObject() instanceof BaseObject baseObject) {
baseObject.setDisplayed(displayed);
}
}
private BasePositionObject getFirstButton(VsmxBaseObject object) { private BasePositionObject getFirstButton(VsmxBaseObject object) {
if (!object.hasPropertyValue(Resource.childrenName)) { if (!object.hasPropertyValue(Resource.childrenName)) {
return null; return null;
@@ -6,6 +6,7 @@ import dev.quoije.umd.rco.type.EventType;
import dev.quoije.umd.rco.type.ImageType; import dev.quoije.umd.rco.type.ImageType;
import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject; import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
import dev.quoije.umd.rco.vsmx.objects.Controller;
/** A user button object (navigation with onLeft/onRight/onUp/onDown events). */ /** A user button object (navigation with onLeft/onRight/onUp/onDown events). */
public class UButtonObject extends BasePositionObject { public class UButtonObject extends BasePositionObject {
@@ -39,6 +40,14 @@ public class UButtonObject extends BasePositionObject {
onDisplayUpdated(); onDisplayUpdated();
} }
@Override
public void setController(Controller controller) {
super.setController(controller);
if (controller != null) {
controller.registerFocusable(this);
}
}
@Override @Override
public void setFocus() { public void setFocus() {
trigger(onFocusIn); trigger(onFocusIn);
@@ -58,21 +67,37 @@ public class UButtonObject extends BasePositionObject {
@Override @Override
public void onUp() { public void onUp() {
trigger(onUp); if (onUp != null && onUp.getEvent() != null) {
trigger(onUp);
} else if (controller != null) {
controller.moveFocus(this, 0, 1);
}
} }
@Override @Override
public void onDown() { public void onDown() {
trigger(onDown); if (onDown != null && onDown.getEvent() != null) {
trigger(onDown);
} else if (controller != null) {
controller.moveFocus(this, 0, -1);
}
} }
@Override @Override
public void onLeft() { public void onLeft() {
trigger(onLeft); if (onLeft != null && onLeft.getEvent() != null) {
trigger(onLeft);
} else if (controller != null) {
controller.moveFocus(this, -1, 0);
}
} }
@Override @Override
public void onRight() { public void onRight() {
trigger(onRight); if (onRight != null && onRight.getEvent() != null) {
trigger(onRight);
} else if (controller != null) {
controller.moveFocus(this, 1, 0);
}
} }
} }
@@ -498,6 +498,19 @@ public final class VsmxInterpreter {
/** Interprets a function body in a fresh call frame. */ /** Interprets a function body in a fresh call frame. */
public synchronized void interpretFunction(VsmxFunction function, VsmxBaseObject object, VsmxBaseObject[] arguments) { public synchronized void interpretFunction(VsmxFunction function, VsmxBaseObject object, VsmxBaseObject[] arguments) {
// This entry point can be invoked re-entrantly (FireEventAnim, timers,
// movie callbacks all run from inside an already-running script), so the
// outer execution state must be preserved and restored. Without this,
// the inner RETURN would set {@code exit} and truncate the outer script.
int savedPc = pc;
boolean savedExit = exit;
Deque<VsmxBaseObject> savedStack = stack;
Deque<VsmxCallState> savedCallStates = callStates;
VsmxCallState savedCallState = callState;
callStates = new ArrayDeque<>();
stack = null;
callState = null;
pushCallState(object, function.getLocalVars(), false, true); pushCallState(object, function.getLocalVars(), false, true);
for (int i = 1; i <= function.getArgs(); i++) { for (int i = 1; i <= function.getArgs(); i++) {
if (arguments == null || i > arguments.length) { if (arguments == null || i > arguments.length) {
@@ -508,6 +521,12 @@ public final class VsmxInterpreter {
} }
pc = function.getStartLine(); pc = function.getStartLine();
interpret(); interpret();
pc = savedPc;
exit = savedExit;
stack = savedStack;
callStates = savedCallStates;
callState = savedCallState;
} }
/** Interprets a script event handler by name (e.g. "script:/main/onPush"). */ /** Interprets a script event handler by name (e.g. "script:/main/onPush"). */
@@ -10,6 +10,9 @@ import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNull; import dev.quoije.umd.rco.vsmx.interpreter.VsmxNull;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxString; import dev.quoije.umd.rco.vsmx.interpreter.VsmxString;
import java.util.ArrayList;
import java.util.List;
/** The controller native object: input focus and resource management. */ /** The controller native object: input focus and resource management. */
public class Controller extends BaseNativeObject { public class Controller extends BaseNativeObject {
@@ -20,6 +23,7 @@ public class Controller extends BaseNativeObject {
private VsmxBaseObject userData; private VsmxBaseObject userData;
private String resource; private String resource;
private BasePositionObject focus; private BasePositionObject focus;
private final List<BasePositionObject> focusables = new ArrayList<>();
public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) { public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) {
Controller controller = new Controller(interpreter, menuHost); Controller controller = new Controller(interpreter, menuHost);
@@ -79,6 +83,63 @@ public class Controller extends BaseNativeObject {
return focus; return focus;
} }
/** Registers a focusable object so default (geometric) focus traversal can reach it. */
public void registerFocusable(BasePositionObject object) {
synchronized (focusables) {
if (!focusables.contains(object)) {
focusables.add(object);
}
}
}
/**
* Moves focus to the nearest visible, focusable object in the given direction
* when a button has no script handler for that direction. {@code dx}/{@code dy}
* use the framebuffer coordinate system (+X right, +Y up).
*/
public void moveFocus(BasePositionObject from, int dx, int dy) {
int fromX = from.getX() + from.getWidth() / 2;
int fromY = from.getY() + from.getHeight() / 2;
BasePositionObject best = null;
double bestScore = Double.MAX_VALUE;
synchronized (focusables) {
for (BasePositionObject candidate : focusables) {
if (candidate == from || !candidate.isDisplayed() || candidate.getAlpha() <= 0f
|| candidate.getImage() == null) {
continue;
}
int candidateX = candidate.getX() + candidate.getWidth() / 2;
int candidateY = candidate.getY() + candidate.getHeight() / 2;
int offsetX = candidateX - fromX;
int offsetY = candidateY - fromY;
if (dy < 0 && offsetY >= 0) {
continue;
}
if (dy > 0 && offsetY <= 0) {
continue;
}
if (dx < 0 && offsetX >= 0) {
continue;
}
if (dx > 0 && offsetX <= 0) {
continue;
}
double primary = dx != 0 ? java.lang.Math.abs(offsetX) : java.lang.Math.abs(offsetY);
double secondary = dx != 0 ? java.lang.Math.abs(offsetY) : java.lang.Math.abs(offsetX);
double score = primary * 10 + secondary;
if (score < bestScore) {
bestScore = score;
best = candidate;
}
}
}
if (best != null) {
best.setFocus();
}
}
public void onUp() { public void onUp() {
if (focus != null) { if (focus != null) {
focus.onUp(); focus.onUp();