From 20d325d383d7ab0aa8917ca7248cd34091213a83 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Tue, 15 Sep 2026 22:45:04 -0400 Subject: [PATCH] Add native objects and bridge menu to playback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../main/java/dev/quoije/umd/cli/Main.java | 50 ++++- .../quoije/umd/rco/vsmx/INativeFunction.java | 12 ++ .../umd/rco/vsmx/interpreter/VsmxMethod.java | 13 ++ .../vsmx/interpreter/VsmxNativeFunction.java | 37 ++++ .../rco/vsmx/interpreter/VsmxReference.java | 10 + .../rco/vsmx/objects/BaseNativeObject.java | 10 + .../umd/rco/vsmx/objects/Controller.java | 83 ++++++++ .../umd/rco/vsmx/objects/GlobalVariables.java | 75 +++++++ .../dev/quoije/umd/rco/vsmx/objects/Math.java | 50 +++++ .../quoije/umd/rco/vsmx/objects/MenuHost.java | 22 ++ .../umd/rco/vsmx/objects/MoviePlayer.java | 192 ++++++++++++++++++ .../vsmx/objects/NativeFunctionFactory.java | 80 ++++++++ .../quoije/umd/rco/vsmx/objects/Resource.java | 31 +++ .../quoije/umd/rco/vsmx/objects/Timer.java | 79 +++++++ 14 files changed, 742 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/INativeFunction.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeFunction.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/GlobalVariables.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Math.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MenuHost.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MoviePlayer.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/NativeFunctionFactory.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Resource.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java diff --git a/cli/src/main/java/dev/quoije/umd/cli/Main.java b/cli/src/main/java/dev/quoije/umd/cli/Main.java index b501dbe..3c96216 100644 --- a/cli/src/main/java/dev/quoije/umd/cli/Main.java +++ b/cli/src/main/java/dev/quoije/umd/cli/Main.java @@ -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 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()) { diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/INativeFunction.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/INativeFunction.java new file mode 100644 index 0000000..95995f2 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/INativeFunction.java @@ -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); +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java index 0963879..292af7c 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxMethod.java @@ -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; } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeFunction.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeFunction.java new file mode 100644 index 0000000..c7a3658 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxNativeFunction.java @@ -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 + "]"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java index 59a3ade..60ae445 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxReference.java @@ -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); } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java index 828a6cc..be84f43 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/BaseNativeObject.java @@ -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() { } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java new file mode 100644 index 0000000..bbaf73b --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java @@ -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() { + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/GlobalVariables.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/GlobalVariables.java new file mode 100644 index 0000000..a3d4594 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/GlobalVariables.java @@ -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()); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Math.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Math.java new file mode 100644 index 0000000..4089cd5 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Math.java @@ -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())); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MenuHost.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MenuHost.java new file mode 100644 index 0000000..d91be6d --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MenuHost.java @@ -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) { + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MoviePlayer.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MoviePlayer.java new file mode 100644 index 0000000..6cfaf3b --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/MoviePlayer.java @@ -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(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/NativeFunctionFactory.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/NativeFunctionFactory.java new file mode 100644 index 0000000..9fc5c45 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/NativeFunctionFactory.java @@ -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 missing = new ArrayList<>(); + + private NativeFunctionFactory() { + } + + public static NativeFunctionFactory getInstance() { + return SINGLETON; + } + + /** Names of native methods that were requested but not found. */ + public List 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); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Resource.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Resource.java new file mode 100644 index 0000000..95bea60 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Resource.java @@ -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; + } +} 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 new file mode 100644 index 0000000..3868400 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Timer.java @@ -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 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(); + } + } +}