Add JavaFX menu renderer and player window

- RcoRenderer implements Display: draws the live RCO object tree onto a
  Canvas, decodes PNG textures, handles focus and resource changes
  (thread-safe via Platform.runLater).
- PageObject now implements root/children semantics and open/close/display
  so opening a page adds its objects to the renderer.
- Object getImage() returns the ImageObject so the UI can decode PNGs.
- PlayerApp wires menu + video: loads the RCO, runs the VSMX script,
  executes onAutoPlay, plays video via PlaybackEngine with YUV->ARGB
  blitting and PTS pacing, and routes arrow/Enter keys to the controller.
- Runs end-to-end on Batman Begins (window, menu load, playback).
This commit is contained in:
2026-09-15 23:14:18 -04:00
parent 6929d0d2d3
commit a4f0d5992b
8 changed files with 387 additions and 19 deletions
@@ -1,14 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** The rendering surface backing an RCO menu. Implemented by the UI layer. */
public interface Display {
void setFocus(IDisplay display);
void add(IDisplay display);
void add(VsmxBaseObject object);
void remove(IDisplay display);
void remove(VsmxBaseObject object);
void repaint();
@@ -26,7 +26,7 @@ public class ButtonObject extends BasePositionObject {
@Override
public Object getImage() {
return image.getImage() == null ? null : image.getImage().getImage();
return image.getImage();
}
@Override
@@ -13,6 +13,6 @@ public class IconObject extends BasePositionObject {
@Override
public Object getImage() {
return unknownImage16.getImage() == null ? null : unknownImage16.getImage().getImage();
return unknownImage16.getImage();
}
}
@@ -2,9 +2,14 @@
package dev.quoije.umd.rco.object;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.RcoFile;
import dev.quoije.umd.rco.type.EventType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxObject;
import dev.quoije.umd.rco.vsmx.objects.Resource;
/** A menu page. */
public class PageObject extends BaseObject {
@@ -15,8 +20,66 @@ public class PageObject extends BaseObject {
@ObjectField(order = 4) public EventType onContextMenu;
@ObjectField(order = 5) public EventType onActivate;
@Override
public VsmxBaseObject createVsmxObject(VsmxInterpreter interpreter, VsmxBaseObject parent, RcoFile.Entry entry) {
VsmxBaseObject object = super.createVsmxObject(interpreter, parent, entry);
VsmxObject root = new VsmxObject(interpreter, null);
object.setPropertyValue(Resource.rootName, root);
return root;
}
private void display(VsmxBaseObject object) {
if (!object.hasPropertyValue(Resource.childrenName)) {
return;
}
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
for (String childName : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(childName);
display.add(child);
display(child);
}
}
private void hide(VsmxBaseObject object) {
if (!object.hasPropertyValue(Resource.childrenName)) {
return;
}
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
for (String childName : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(childName);
display.remove(child);
hide(child);
}
}
private BasePositionObject getFirstButton(VsmxBaseObject object) {
if (!object.hasPropertyValue(Resource.childrenName)) {
return null;
}
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
for (String childName : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(childName);
if (child instanceof VsmxNativeObject nativeObject && nativeObject.getObject() instanceof UButtonObject button) {
return button;
}
BasePositionObject button = getFirstButton(child);
if (button != null) {
return button;
}
}
return null;
}
public VsmxBaseObject open(VsmxBaseObject object) {
trigger(onInit);
if (display != null && object.hasPropertyValue(Resource.rootName)) {
display(object.getPropertyValue(Resource.rootName));
BasePositionObject button = getFirstButton(object.getPropertyValue(Resource.rootName));
if (button != null) {
button.setFocus();
}
}
return object;
}
@@ -26,6 +89,9 @@ public class PageObject extends BaseObject {
}
public void close(VsmxBaseObject object) {
if (display != null && object.hasPropertyValue(Resource.rootName)) {
hide(object.getPropertyValue(Resource.rootName));
}
trigger(onCancel);
}
}
@@ -13,6 +13,6 @@ public class PlaneObject extends BasePositionObject {
@Override
public Object getImage() {
return image.getImage() == null ? null : image.getImage().getImage();
return image.getImage();
}
}
@@ -21,7 +21,7 @@ public class UButtonObject extends BasePositionObject {
@Override
public Object getImage() {
return image.getImage() == null ? null : image.getImage().getImage();
return image.getImage();
}
@Override