Add native objects and bridge menu to playback

- NativeFunctionFactory resolves script method calls to native Java
  methods via reflection; VsmxNativeFunction wraps them for the VM.
- Native objects: Controller, MoviePlayer, Resource, Math, Timer,
  GlobalVariables, decoupled from the UI via a MenuHost interface.
- BaseNativeObject.callCallback invokes script callbacks like onAutoPlay.
- CLI 'rco' now wires the full menu and executes onAutoPlay headlessly.
- Validated against Batman Begins: onAutoPlay computes stream IDs and
  issues play(playList=3, chapter=1, video=0xE000, audio=0xBD00,
  audioFlag=1, subtitle=-1, subtitleFlag=0) — the anti-piracy warning
  clip with English video/audio.
This commit is contained in:
2026-09-15 22:45:04 -04:00
parent d56f6f12b0
commit 20d325d383
14 changed files with 742 additions and 2 deletions
+48 -2
View File
@@ -13,7 +13,13 @@ import dev.quoije.umd.rco.RcoFile;
import dev.quoije.umd.rco.vsmx.Vsmx;
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.Controller;
import dev.quoije.umd.rco.vsmx.objects.GlobalVariables;
import dev.quoije.umd.rco.vsmx.objects.MenuHost;
import dev.quoije.umd.rco.vsmx.objects.MoviePlayer;
import dev.quoije.umd.rco.vsmx.objects.Resource;
import dev.quoije.umd.sfo.Sfo;
import java.io.ByteArrayOutputStream;
@@ -420,13 +426,53 @@ public final class Main {
VsmxInterpreter interpreter = new VsmxInterpreter();
interpreter.setVsmx(vsmx);
VsmxObject globals = new VsmxObject(interpreter, null);
final List<String> playCalls = new java.util.ArrayList<>();
MenuHost menuHost = new MenuHost() {
@Override
public void playMovie(int playListNumber, int chapterNumber, int videoNumber, int audioNumber,
int audioFlag, int subtitleNumber, int subtitleFlag) {
playCalls.add(String.format("play(playList=%d, chapter=%d, video=%d, audio=%d, audioFlag=%d, subtitle=%d, subtitleFlag=%d)",
playListNumber, chapterNumber, videoNumber, audioNumber, audioFlag, subtitleNumber, subtitleFlag));
}
@Override
public void stopMovie() {
playCalls.add("stop()");
}
@Override
public void changeResource(String resourceName) {
playCalls.add("changeResource(" + resourceName + ")");
}
};
String resourceName = rcoPath.substring(rcoPath.lastIndexOf('/') + 1).replace(".RCO", "");
VsmxNativeObject globals = GlobalVariables.create(interpreter, 854, 480);
VsmxNativeObject controller = Controller.create(interpreter, menuHost, resourceName);
VsmxNativeObject movieplayer = MoviePlayer.create(interpreter, menuHost, controller);
VsmxNativeObject resource = Resource.create(interpreter);
VsmxNativeObject math = dev.quoije.umd.rco.vsmx.objects.Math.create(interpreter);
globals.setPropertyValue(Controller.objectName, controller);
globals.setPropertyValue(MoviePlayer.objectName, movieplayer);
globals.setPropertyValue(Resource.objectName, resource);
globals.setPropertyValue(dev.quoije.umd.rco.vsmx.objects.Math.objectName, math);
interpreter.run(globals);
System.out.printf("global variables (%d):%n", globals.getPropertyNames().size());
for (String varName : globals.getPropertyNames()) {
VsmxBaseObject value = globals.getPropertyValue(varName);
System.out.printf(" %s = %s%n", varName, value);
if (!(value instanceof VsmxNativeObject)) {
System.out.printf(" %s = %s%n", varName, value);
}
}
// Execute the menu's autoplay handler (stored on the controller).
((Controller) controller.getObject()).callCallback(interpreter, "onAutoPlay", null);
System.out.printf("onAutoPlay play calls (%d):%n", playCalls.size());
for (String call : playCalls) {
System.out.println(" " + call);
}
if (!interpreter.getWarnings().isEmpty()) {
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** A native (Java-implemented) function callable from the VSMX script. */
public interface INativeFunction {
int getArgs();
VsmxBaseObject call(VsmxBaseObject[] arguments);
}
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import dev.quoije.umd.rco.vsmx.INativeFunction;
import dev.quoije.umd.rco.vsmx.objects.NativeFunctionFactory;
/**
* A method reference: an object plus a method name, resolved at call time.
*/
@@ -58,6 +61,16 @@ public class VsmxMethod extends VsmxBaseObject {
return (VsmxFunction) object;
}
INativeFunction nativeFunction = null;
if (object instanceof VsmxNativeObject nativeObject) {
nativeFunction = NativeFunctionFactory.getInstance().getNativeFunction(nativeObject, name, numberOfArguments);
} else {
nativeFunction = NativeFunctionFactory.getInstance().getNativeFunction(object, name, numberOfArguments);
}
if (nativeFunction != null) {
return new VsmxNativeFunction(interpreter, nativeFunction);
}
return null;
}
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import dev.quoije.umd.rco.vsmx.INativeFunction;
/** A function value backed by a native Java method. */
public final class VsmxNativeFunction extends VsmxFunction {
private final INativeFunction nativeFunction;
private final VsmxBaseObject[] arguments;
private VsmxBaseObject returnValue;
public VsmxNativeFunction(VsmxInterpreter interpreter, INativeFunction nativeFunction) {
super(interpreter, nativeFunction.getArgs(), 0, -1);
this.nativeFunction = nativeFunction;
this.arguments = new VsmxBaseObject[nativeFunction.getArgs() + 1];
}
@Override
public void call(VsmxCallState callState) {
arguments[0] = callState.getThisObject();
for (int i = 1; i < arguments.length; i++) {
arguments[i] = callState.getLocalVar(i);
}
returnValue = nativeFunction.call(arguments);
}
@Override
public VsmxBaseObject getReturnValue() {
return returnValue;
}
@Override
public String toString() {
return "VsmxNativeFunction[" + nativeFunction + ", returnValue=" + returnValue + "]";
}
}
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.interpreter;
import dev.quoije.umd.rco.vsmx.INativeFunction;
import dev.quoije.umd.rco.vsmx.objects.NativeFunctionFactory;
/**
* A reference to a property or array index of an object, so assignments and
* increments/decrements can write back through it.
@@ -48,6 +51,13 @@ public class VsmxReference extends VsmxBaseObject {
if (refProperty == null) {
return refObject.getPropertyValue(refIndex);
}
if (!refObject.hasPropertyValue(refProperty) && refObject instanceof VsmxNativeObject nativeObject) {
INativeFunction nativeFunction = NativeFunctionFactory.getInstance()
.getNativeFunction(nativeObject, refProperty, numberOfArguments);
if (nativeFunction != null) {
return new VsmxNativeFunction(interpreter, nativeFunction);
}
}
return refObject.getPropertyValue(refProperty);
}
@@ -2,6 +2,8 @@
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxFunction;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
/**
@@ -29,6 +31,14 @@ public abstract class BaseNativeObject {
this.name = name;
}
/** Invokes a script callback property (e.g. "onAutoPlay") stored on this object. */
public void callCallback(VsmxInterpreter interpreter, String callbackName, VsmxBaseObject[] arguments) {
VsmxBaseObject callback = object.getPropertyValue(callbackName);
if (callback instanceof VsmxFunction function) {
interpreter.interpretFunction(function, null, arguments);
}
}
public void onDisplayUpdated() {
}
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxArray;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBoolean;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNull;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxString;
/** The controller native object: input focus and resource management. */
public class Controller extends BaseNativeObject {
public static final String objectName = "controller";
private final VsmxInterpreter interpreter;
private final MenuHost menuHost;
private VsmxBaseObject userData;
private String resource;
public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) {
Controller controller = new Controller(interpreter, menuHost);
VsmxNativeObject object = new VsmxNativeObject(interpreter, controller);
controller.setObject(object);
controller.resource = resource;
object.setPropertyValue("onChangeResource", VsmxNull.singleton);
object.setPropertyValue("onMenu", VsmxNull.singleton);
object.setPropertyValue("onExit", VsmxNull.singleton);
object.setPropertyValue("onAutoPlay", VsmxNull.singleton);
object.setPropertyValue("onContinuePlay", VsmxNull.singleton);
return object;
}
private Controller(VsmxInterpreter interpreter, MenuHost menuHost) {
this.interpreter = interpreter;
this.menuHost = menuHost;
this.userData = new VsmxArray(interpreter);
}
public VsmxInterpreter getInterpreter() {
return interpreter;
}
public String getResource() {
return resource;
}
public VsmxBaseObject getUserData(VsmxBaseObject object) {
return userData;
}
public VsmxBaseObject setUserData(VsmxBaseObject object, VsmxBaseObject userData) {
this.userData = userData;
return VsmxBoolean.getValue(true);
}
public VsmxBaseObject changeResource(VsmxBaseObject object, VsmxBaseObject newResource) {
String resourceName = newResource.getStringValue();
if (!resource.equals(resourceName)) {
resource = resourceName;
menuHost.changeResource(resourceName);
}
return VsmxBoolean.getValue(true);
}
public void onUp() {
}
public void onDown() {
}
public void onLeft() {
}
public void onRight() {
}
public void onPush() {
}
}
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxArray;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBoolean;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNumber;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxUndefined;
/** The global scope object, exposing standard globals and console helpers. */
public class GlobalVariables extends BaseNativeObject {
private final StringBuilder log = new StringBuilder();
/** Creates the global variables object, including standard globals and the timer. */
public static VsmxNativeObject create(VsmxInterpreter interpreter, int screenWidth, int screenHeight) {
GlobalVariables globalVariables = new GlobalVariables();
VsmxNativeObject object = new VsmxNativeObject(interpreter, globalVariables);
globalVariables.setObject(object);
object.setPropertyValue("undefined", VsmxUndefined.singleton);
object.setPropertyValue("Array", new VsmxArray(interpreter));
object.setPropertyValue("Object", new VsmxObject(interpreter, null));
object.setPropertyValue("timer", Timer.create(interpreter));
object.setPropertyValue("x", new VsmxNumber(interpreter, 0));
object.setPropertyValue("y", new VsmxNumber(interpreter, 0));
object.setPropertyValue("width", new VsmxNumber(interpreter, screenWidth));
object.setPropertyValue("height", new VsmxNumber(interpreter, screenHeight));
return object;
}
public String getLog() {
return log.toString();
}
public void writeln(VsmxBaseObject object) {
log.append('\n');
}
public void writeln(VsmxBaseObject object, VsmxBaseObject s1) {
log.append(s1.getStringValue()).append('\n');
}
public void writeln(VsmxBaseObject object, VsmxBaseObject s1, VsmxBaseObject s2) {
log.append(s1.getStringValue()).append(s2.getStringValue()).append('\n');
}
public void writeln(VsmxBaseObject object, VsmxBaseObject s1, VsmxBaseObject s2, VsmxBaseObject s3) {
log.append(s1.getStringValue()).append(s3.getStringValue()).append('\n');
}
public VsmxBaseObject parseFloat(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(object.getInterpreter(), value.getFloatValue());
}
public VsmxBaseObject parseInt(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(object.getInterpreter(), value.getIntValue());
}
public VsmxBaseObject isNaN(VsmxBaseObject object, VsmxBaseObject value) {
return VsmxBoolean.getValue(Float.isNaN(value.getFloatValue()));
}
public VsmxBaseObject Float(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(object.getInterpreter(), value.getFloatValue());
}
public VsmxBaseObject Int(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(object.getInterpreter(), value.getIntValue());
}
}
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
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.VsmxNumber;
import java.util.Random;
/** The Math native object. */
public class Math extends BaseNativeObject {
public static final String objectName = "Math";
private final VsmxInterpreter interpreter;
private final Random random = new Random();
public static VsmxNativeObject create(VsmxInterpreter interpreter) {
Math math = new Math(interpreter);
VsmxNativeObject object = new VsmxNativeObject(interpreter, math);
math.setObject(object);
object.setPropertyValue("PI", new VsmxNumber(interpreter, (float) java.lang.Math.PI));
return object;
}
private Math(VsmxInterpreter interpreter) {
this.interpreter = interpreter;
}
public VsmxBaseObject random(VsmxBaseObject object) {
return new VsmxNumber(interpreter, random.nextFloat());
}
public VsmxBaseObject floor(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(interpreter, (float) java.lang.Math.floor(value.getFloatValue()));
}
public VsmxBaseObject abs(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(interpreter, (float) java.lang.Math.abs(value.getFloatValue()));
}
public VsmxBaseObject sin(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(interpreter, (float) java.lang.Math.sin(value.getFloatValue()));
}
public VsmxBaseObject cos(VsmxBaseObject object, VsmxBaseObject value) {
return new VsmxNumber(interpreter, (float) java.lang.Math.cos(value.getFloatValue()));
}
}
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
/**
* Bridge from the menu script's MoviePlayer to the actual playback engine.
* Implemented by the UI/player layer.
*/
public interface MenuHost {
void playMovie(int playListNumber, int chapterNumber, int videoNumber,
int audioNumber, int audioFlag, int subtitleNumber, int subtitleFlag);
void stopMovie();
void changeResource(String resourceName);
default void onPlayListEnd(int playListNumber) {
}
default void onChapter(int chapterNumber) {
}
}
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxArray;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxFunction;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNumber;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxString;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxUndefined;
/** The movie player native object, bridging to the playback engine via MenuHost. */
public class MoviePlayer extends BaseNativeObject {
public static final int DEFAULT_WIDTH = 854;
public static final int DEFAULT_HEIGHT = 480;
public static final String objectName = "movieplayer";
private final VsmxInterpreter interpreter;
private final MenuHost menuHost;
private final VsmxNativeObject controller;
private boolean playing;
private boolean menuMode;
private int playListNumber;
private int chapterNumber;
private int videoNumber;
private int audioNumber;
private int audioFlag;
private int subtitleNumber;
private int subtitleFlag;
private int width = DEFAULT_WIDTH;
private int height = DEFAULT_HEIGHT;
private int x;
private int y;
public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, VsmxNativeObject controller) {
MoviePlayer moviePlayer = new MoviePlayer(interpreter, menuHost, controller);
VsmxNativeObject object = new VsmxNativeObject(interpreter, moviePlayer);
moviePlayer.setObject(object);
object.setPropertyValue("audioLanguageCode", new VsmxString(interpreter, "en"));
object.setPropertyValue("subtitleLanguageCode", new VsmxString(interpreter, "en"));
return object;
}
private MoviePlayer(VsmxInterpreter interpreter, MenuHost menuHost, VsmxNativeObject controller) {
this.interpreter = interpreter;
this.menuHost = menuHost;
this.controller = controller;
}
public void play(VsmxBaseObject object,
VsmxBaseObject pauseMode,
VsmxBaseObject menuMode,
VsmxBaseObject playListNumber,
VsmxBaseObject chapterNumber,
VsmxBaseObject videoNumber,
VsmxBaseObject audioNumber,
VsmxBaseObject audioFlag,
VsmxBaseObject subtitleNumber,
VsmxBaseObject subtitleFlag,
VsmxBaseObject unknownBool) {
playing = true;
boolean previousMenuMode = this.menuMode;
this.menuMode = menuMode.getBooleanValue();
this.playListNumber = playListNumber.getIntValue();
this.chapterNumber = chapterNumber.getIntValue();
this.videoNumber = videoNumber.getIntValue();
this.audioNumber = audioNumber.getIntValue();
this.audioFlag = audioFlag.getIntValue();
this.subtitleNumber = subtitleNumber.getIntValue();
this.subtitleFlag = subtitleFlag.getIntValue();
menuHost.playMovie(this.playListNumber, this.chapterNumber, this.videoNumber,
this.audioNumber, this.audioFlag, this.subtitleNumber, this.subtitleFlag);
if (!previousMenuMode && this.menuMode) {
VsmxBaseObject callback = controller.getPropertyValue("onMenu");
if (callback instanceof VsmxFunction function) {
interpreter.interpretFunction(function, null, new VsmxBaseObject[0]);
}
}
}
public void stop(VsmxBaseObject object, VsmxBaseObject unknownInt, VsmxBaseObject unknownBool) {
playing = false;
menuHost.stopMovie();
}
public void resume(VsmxBaseObject object) {
playing = true;
}
public VsmxBaseObject getResumeInfo(VsmxBaseObject object) {
if (playing) {
VsmxBaseObject resumeInfo = new VsmxObject(interpreter, "ResumeInfo");
resumeInfo.setPropertyValue("playListNumber", new VsmxNumber(interpreter, playListNumber));
return resumeInfo;
}
return VsmxUndefined.singleton;
}
public void changeResumeInfo(VsmxBaseObject object, VsmxBaseObject videoNumber, VsmxBaseObject audioNumber,
VsmxBaseObject audioFlag, VsmxBaseObject subtitleNumber, VsmxBaseObject subtitleFlag) {
this.videoNumber = videoNumber.getIntValue();
this.audioNumber = audioNumber.getIntValue();
this.audioFlag = audioFlag.getIntValue();
this.subtitleNumber = subtitleNumber.getIntValue();
this.subtitleFlag = subtitleFlag.getIntValue();
}
public VsmxBaseObject getPlayerStatus(VsmxBaseObject object) {
if (playing) {
VsmxBaseObject status = new VsmxObject(interpreter, "PlayerStatus");
status.setPropertyValue("playListNumber", new VsmxNumber(interpreter, playListNumber));
status.setPropertyValue("chapterNumber", new VsmxNumber(interpreter, chapterNumber));
status.setPropertyValue("videoNumber", new VsmxNumber(interpreter, videoNumber));
status.setPropertyValue("audioNumber", new VsmxNumber(interpreter, audioNumber));
status.setPropertyValue("audioFlag", new VsmxNumber(interpreter, audioFlag));
status.setPropertyValue("subtitleNumber", new VsmxNumber(interpreter, subtitleNumber));
status.setPropertyValue("subtitleFlag", new VsmxNumber(interpreter, subtitleFlag));
return status;
}
return VsmxUndefined.singleton;
}
public VsmxBaseObject getSize(VsmxBaseObject object) {
VsmxArray size = new VsmxArray(interpreter, 2);
size.setPropertyValue(0, new VsmxNumber(interpreter, width));
size.setPropertyValue(1, new VsmxNumber(interpreter, height));
return size;
}
public void setSize(VsmxBaseObject object, VsmxBaseObject width, VsmxBaseObject height) {
this.width = width.getIntValue();
this.height = height.getIntValue();
}
public VsmxBaseObject getPos(VsmxBaseObject object) {
VsmxArray pos = new VsmxArray(interpreter, 2);
pos.setPropertyValue(0, new VsmxNumber(interpreter, x));
pos.setPropertyValue(1, new VsmxNumber(interpreter, y));
return pos;
}
public void setPos(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y) {
this.x = x.getIntValue();
this.y = y.getIntValue();
}
public void onPlayListEnd(int playListNumber) {
VsmxBaseObject callback = getObject().getPropertyValue("onPlayListEnd");
if (callback instanceof VsmxFunction function) {
VsmxBaseObject argument = new VsmxObject(interpreter, null);
argument.setPropertyValue("playListNumber", new VsmxNumber(interpreter, playListNumber));
interpreter.interpretFunction(function, null, new VsmxBaseObject[]{argument});
}
}
public void onChapter(int chapterNumber) {
VsmxBaseObject callback = getObject().getPropertyValue("onChapter");
if (callback instanceof VsmxFunction function) {
VsmxBaseObject argument = new VsmxObject(interpreter, null);
argument.setPropertyValue("chapterNumber", new VsmxNumber(interpreter, chapterNumber));
interpreter.interpretFunction(function, null, new VsmxBaseObject[]{argument});
}
}
public void onUp() {
((Controller) controller.getObject()).onUp();
}
public void onDown() {
((Controller) controller.getObject()).onDown();
}
public void onLeft() {
((Controller) controller.getObject()).onLeft();
}
public void onRight() {
((Controller) controller.getObject()).onRight();
}
public void onPush() {
((Controller) controller.getObject()).onPush();
}
}
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.INativeFunction;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Resolves script method calls to native Java methods via reflection.
* Ported from JPCSP.
*/
public final class NativeFunctionFactory {
private static final NativeFunctionFactory SINGLETON = new NativeFunctionFactory();
private final List<String> missing = new ArrayList<>();
private NativeFunctionFactory() {
}
public static NativeFunctionFactory getInstance() {
return SINGLETON;
}
/** Names of native methods that were requested but not found. */
public List<String> getMissing() {
return missing;
}
private record NativeFunction(Object object, Method method, int args) implements INativeFunction {
@Override
public int getArgs() {
return args;
}
@Override
public VsmxBaseObject call(VsmxBaseObject[] arguments) {
VsmxBaseObject returnValue = null;
try {
Object result = method.invoke(object, (Object[]) arguments);
if (result instanceof VsmxBaseObject obj) {
returnValue = obj;
}
} catch (ReflectiveOperationException e) {
// Ignore
}
return returnValue;
}
}
private INativeFunction getNativeFunctionInterface(Object object, String name, int numberOfArguments) {
for (int args = numberOfArguments + 1; args < 12; args++) {
Class<?>[] arguments = new Class<?>[args];
for (int i = 0; i < arguments.length; i++) {
arguments[i] = VsmxBaseObject.class;
}
try {
Method method = object.getClass().getMethod(name, arguments);
return new NativeFunction(object, method, args - 1);
} catch (NoSuchMethodException e) {
// Ignore
}
}
missing.add(object.getClass().getSimpleName() + "." + name + "(args=" + (numberOfArguments + 1) + ")");
return null;
}
public INativeFunction getNativeFunction(VsmxNativeObject object, String name, int numberOfArguments) {
BaseNativeObject nativeObject = object.getObject();
return getNativeFunctionInterface(nativeObject, name, numberOfArguments);
}
public INativeFunction getNativeFunction(VsmxBaseObject object, String name, int numberOfArguments) {
return getNativeFunctionInterface(object, name, numberOfArguments);
}
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxObject;
/**
* The resource native object, exposing the RCO object tables to the script.
* The object tree population is layered in with the renderer.
*/
public class Resource extends BaseNativeObject {
public static final String objectName = "resource";
public static final String rootName = "root";
public static final String childrenName = "children";
public static final String textureName = "texture";
public static VsmxNativeObject create(VsmxInterpreter interpreter) {
Resource resource = new Resource();
VsmxNativeObject object = new VsmxNativeObject(interpreter, resource);
resource.setObject(object);
object.setPropertyValue("pagetable", new VsmxObject(interpreter, null));
object.setPropertyValue("animtable", new VsmxObject(interpreter, null));
object.setPropertyValue("soundtable", new VsmxObject(interpreter, null));
object.setPropertyValue("texturetable", new VsmxObject(interpreter, null));
return object;
}
}
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.vsmx.objects;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxArray;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxFunction;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxInterpreter;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
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. */
public class Timer extends BaseNativeObject {
private final VsmxInterpreter interpreter;
private int currentTimerId;
private final Map<Integer, Thread> timers = new HashMap<>();
public static VsmxNativeObject create(VsmxInterpreter interpreter) {
Timer timer = new Timer(interpreter);
VsmxNativeObject object = new VsmxNativeObject(interpreter, timer);
timer.setObject(object);
return object;
}
private Timer(VsmxInterpreter interpreter) {
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(() -> {
try {
Thread.sleep(millis);
} 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();
VsmxArray result = new VsmxArray(interpreter, 1);
result.setPropertyValue(0, new VsmxNumber(interpreter, id));
return result;
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval) {
return setInterval(object, function, interval, new VsmxBaseObject[0]);
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1});
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1, VsmxBaseObject param2) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2});
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1, VsmxBaseObject param2, VsmxBaseObject param3) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3});
}
public void clearInterval(VsmxBaseObject object, VsmxBaseObject id) {
int idValue = id.getPropertyValue(0).getIntValue();
Thread thread = timers.remove(idValue);
if (thread != null) {
thread.interrupt();
}
}
}