diff --git a/README.md b/README.md index 1270942..5f0f56d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # useless-umd-player +[grrr](assets/grrr.png) + +![AI Generated](https://raw.githubusercontent.com/dotnetdave/ai-usage-badges/refs/heads/main/badges/png/1x/ai-generated.png) + a standalone player for PSP UMD Video ISO images: open a disc, render its real menu, and play the movie (H.264 video + ATRAC3+ audio) with mouse and keyboard/media-key controls. diff --git a/assets/grrr.png b/assets/grrr.png new file mode 100644 index 0000000..8e89fdb Binary files /dev/null and b/assets/grrr.png differ diff --git a/core/src/main/java/dev/quoije/umd/rco/Display.java b/core/src/main/java/dev/quoije/umd/rco/Display.java new file mode 100644 index 0000000..a7998a4 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/Display.java @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco; + +/** The rendering surface backing an RCO menu. Implemented by the UI layer. */ +public interface Display { + + void setFocus(IDisplay display); + + void add(IDisplay display); + + void remove(IDisplay display); + + void repaint(); + + void changeResource(); +} diff --git a/core/src/main/java/dev/quoije/umd/rco/IDisplay.java b/core/src/main/java/dev/quoije/umd/rco/IDisplay.java new file mode 100644 index 0000000..c343ef2 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/IDisplay.java @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco; + +/** A renderable RCO object (plane, button, text, ...). */ +public interface IDisplay { + + Object getImage(); + + Object getAnimImage(); + + int getX(); + + int getY(); + + int getWidth(); + + int getHeight(); + + float getAlpha(); + + void setFocus(); + + void focusOut(); + + void onUp(); + + void onDown(); + + void onLeft(); + + void onRight(); + + void onPush(); +} diff --git a/core/src/main/java/dev/quoije/umd/rco/ObjectField.java b/core/src/main/java/dev/quoije/umd/rco/ObjectField.java new file mode 100644 index 0000000..966580c --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/ObjectField.java @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Orders the typed fields of an RCO object record for serialization. */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ObjectField { + int order(); +} diff --git a/core/src/main/java/dev/quoije/umd/rco/RcoContext.java b/core/src/main/java/dev/quoije/umd/rco/RcoContext.java new file mode 100644 index 0000000..ef6149b --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/RcoContext.java @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco; + +import dev.quoije.umd.rco.object.BaseObject; +import dev.quoije.umd.rco.object.ImageObject; + +import java.util.Map; + +/** + * Mutable read cursor and shared reference tables used when deserializing + * RCO object and animation records. + */ +public final class RcoContext { + + public byte[] buffer; + public int offset; + public Map events; + public Map images; + public Map objects; + + public RcoContext(byte[] buffer, int offset, Map events, + Map images, Map objects) { + this.buffer = buffer; + this.offset = offset; + this.events = events; + this.images = images; + this.objects = objects; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java b/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java new file mode 100644 index 0000000..522353d --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.object; + +import dev.quoije.umd.rco.Display; +import dev.quoije.umd.rco.ObjectField; +import dev.quoije.umd.rco.RcoContext; +import dev.quoije.umd.rco.RcoFile; +import dev.quoije.umd.rco.type.BaseType; +import dev.quoije.umd.rco.type.EventType; +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.VsmxString; +import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject; +import dev.quoije.umd.rco.vsmx.objects.Controller; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; + +/** + * Base of RCO object records (pages, planes, buttons, ...). Fields are + * declared as {@link BaseType} subclasses and read/measured via reflection, + * mirroring JPCSP's object model. + */ +public abstract class BaseObject extends BaseNativeObject implements IDisplay { + + protected Display display; + protected Controller controller; + private String name; + private BaseNativeObject parent; + + private Field[] getSortedFields() { + Field[] fields = getClass().getFields(); + Arrays.sort(fields, Comparator.comparingInt(f -> { + ObjectField annotation = f.getAnnotation(ObjectField.class); + return annotation == null ? Integer.MAX_VALUE : annotation.order(); + })); + return fields; + } + + /** Deserializes all typed fields from the record data. */ + public void read(RcoContext context) { + for (Field field : getSortedFields()) { + if (BaseType.class.isAssignableFrom(field.getType())) { + try { + BaseType type = (BaseType) field.get(this); + if (type == null) { + type = (BaseType) field.getType().getDeclaredConstructor().newInstance(); + field.set(this, type); + } + type.read(context); + } catch (ReflectiveOperationException e) { + // Ignore + } + } + } + } + + /** Size in bytes of the serialized typed fields. */ + public int size() { + int size = 0; + for (Field field : getSortedFields()) { + if (BaseType.class.isAssignableFrom(field.getType())) { + try { + BaseType type = (BaseType) field.get(this); + if (type == null) { + type = (BaseType) field.getType().getDeclaredConstructor().newInstance(); + } + size += type.size(); + } catch (ReflectiveOperationException e) { + // Ignore + } + } + } + return size; + } + + /** Resolves reference fields against the RCO tables. */ + public void init(RcoContext context) { + for (Field field : getSortedFields()) { + if (BaseType.class.isAssignableFrom(field.getType())) { + try { + BaseType type = (BaseType) field.get(this); + if (type != null) { + type.init(context); + } + } catch (ReflectiveOperationException e) { + // Ignore + } + } + } + } + + /** Creates the VSMX-side object and registers it under the entry label. */ + public VsmxBaseObject createVsmxObject(VsmxInterpreter interpreter, VsmxBaseObject parent, RcoFile.Entry entry) { + VsmxNativeObject object = new VsmxNativeObject(interpreter, this); + setObject(object); + if (entry.label != null) { + name = entry.label; + object.setPropertyValue("name", new VsmxString(interpreter, entry.label)); + parent.setPropertyValue(entry.label, object); + } + if (entry.parent != null && entry.parent.vsmxObject instanceof VsmxNativeObject parentNative) { + setParent(parentNative.getObject()); + } + return object; + } + + public void setDisplay(Display display) { + this.display = display; + } + + public void setController(Controller controller) { + this.controller = controller; + } + + public BaseNativeObject getParent() { + return parent; + } + + public void setParent(BaseNativeObject parent) { + this.parent = parent; + } + + public String getName() { + return name; + } + + protected void trigger(EventType event) { + if (event.getEvent() != null && controller != null) { + controller.getInterpreter().interpretScript(getObject(), event.getEvent()); + } + } + + protected void onDisplayUpdated() { + if (display != null) { + display.repaint(); + } + } + + @Override + public Object getImage() { + return null; + } + + @Override + public Object getAnimImage() { + return getImage(); + } + + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return 0; + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public int getHeight() { + return 0; + } + + @Override + public float getAlpha() { + return 1f; + } + + @Override + public void setFocus() { + } + + @Override + public void focusOut() { + } + + @Override + public void onUp() { + } + + @Override + public void onDown() { + } + + @Override + public void onLeft() { + } + + @Override + public void onRight() { + } + + @Override + public void onPush() { + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[name=" + name + "]"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/object/ImageObject.java b/core/src/main/java/dev/quoije/umd/rco/object/ImageObject.java new file mode 100644 index 0000000..9377a4a --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/object/ImageObject.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.object; + +/** An image entry from the RCO IMG table (raw bytes until decoded by the UI). */ +public final class ImageObject { + + private final byte[] data; + + public ImageObject(byte[] data) { + this.data = data; + } + + public byte[] getData() { + return data; + } + + /** Returns the decoded image, or null (decoded lazily by the renderer). */ + public Object getImage() { + return null; + } + + @Override + public String toString() { + return "ImageObject[" + (data == null ? 0 : data.length) + " bytes]"; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/BaseReferenceType.java b/core/src/main/java/dev/quoije/umd/rco/type/BaseReferenceType.java new file mode 100644 index 0000000..017c090 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/BaseReferenceType.java @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +import dev.quoije.umd.rco.RcoContext; +import dev.quoije.umd.rco.object.BaseObject; +import dev.quoije.umd.rco.object.ImageObject; + +/** + * An 8-byte reference field: a 16-bit reference type, a 16-bit unknown + * value, and a 32-bit reference index. + */ +public class BaseReferenceType extends BaseType { + + public static final int REFERENCE_TYPE_NONE = 0xFFFF; + public static final int REFERENCE_TYPE_EVENT = 0x400; + public static final int REFERENCE_TYPE_TEXT = 0x401; + public static final int REFERENCE_TYPE_IMAGE = 0x402; + public static final int REFERENCE_TYPE_MODEL = 0x403; + public static final int REFERENCE_TYPE_FONT = 0x405; + public static final int REFERENCE_TYPE_OBJECT = 0x407; + public static final int REFERENCE_TYPE_ANIM = 0x408; + public static final int REFERENCE_TYPE_POSITION_OBJECT = 0x409; + + public int referenceType; + public int unknownShort; + protected String event; + protected BaseObject object; + protected ImageObject image; + + @Override + public int size() { + return 8; + } + + @Override + public void read(RcoContext context) { + referenceType = read16(context); + unknownShort = read16(context); + super.read(context); + } + + @Override + public void init(RcoContext context) { + switch (referenceType) { + case REFERENCE_TYPE_NONE -> { } + case REFERENCE_TYPE_EVENT -> event = context.events.get(value); + case REFERENCE_TYPE_OBJECT, REFERENCE_TYPE_POSITION_OBJECT, REFERENCE_TYPE_ANIM -> + object = context.objects.get(value); + case REFERENCE_TYPE_IMAGE -> image = context.images.get(value); + default -> { } + } + super.init(context); + } + + public String getEvent() { + return event; + } + + public BaseObject getObject() { + return object; + } + + public ImageObject getImage() { + return image; + } + + @Override + public String toString() { + return "referenceType=0x" + Integer.toHexString(referenceType) + + ", short1=0x" + Integer.toHexString(unknownShort) + + ", value=0x" + Integer.toHexString(value); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/BaseType.java b/core/src/main/java/dev/quoije/umd/rco/type/BaseType.java new file mode 100644 index 0000000..6522f54 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/BaseType.java @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +import dev.quoije.umd.rco.RcoContext; + +/** Base of the typed fields inside an RCO object record. */ +public class BaseType { + + protected int value; + + public int size() { + return 4; + } + + protected int read8(RcoContext context) { + return context.buffer[context.offset++] & 0xFF; + } + + protected int read16(RcoContext context) { + return read8(context) | (read8(context) << 8); + } + + protected int read32(RcoContext context) { + return read16(context) | (read16(context) << 16); + } + + public void read(RcoContext context) { + value = read32(context); + } + + public void init(RcoContext context) { + } + + public int getIntValue() { + return value; + } + + public void setIntValue(int value) { + this.value = value; + } + + public float getFloatValue() { + return (float) value; + } + + public void setFloatValue(float value) { + this.value = (int) value; + } + + @Override + public String toString() { + return "value=0x" + Integer.toHexString(getIntValue()); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/EventType.java b/core/src/main/java/dev/quoije/umd/rco/type/EventType.java new file mode 100644 index 0000000..975c5d1 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/EventType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to a script event. */ +public class EventType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/FloatType.java b/core/src/main/java/dev/quoije/umd/rco/type/FloatType.java new file mode 100644 index 0000000..c4e1a03 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/FloatType.java @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** A 32-bit floating-point field stored as IEEE 754 bits. */ +public class FloatType extends BaseType { + + @Override + public float getFloatValue() { + return Float.intBitsToFloat(super.getIntValue()); + } + + @Override + public void setFloatValue(float value) { + super.setIntValue(Float.floatToRawIntBits(value)); + } + + @Override + public int getIntValue() { + return (int) getFloatValue(); + } + + @Override + public void setIntValue(int value) { + setFloatValue((float) value); + } + + @Override + public String toString() { + return "value=" + getFloatValue(); + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/ImageType.java b/core/src/main/java/dev/quoije/umd/rco/type/ImageType.java new file mode 100644 index 0000000..b262dab --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/ImageType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to an image entry. */ +public class ImageType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/IntType.java b/core/src/main/java/dev/quoije/umd/rco/type/IntType.java new file mode 100644 index 0000000..674f8ba --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/IntType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** A 32-bit integer field. */ +public class IntType extends BaseType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/ModelType.java b/core/src/main/java/dev/quoije/umd/rco/type/ModelType.java new file mode 100644 index 0000000..59ea6b6 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/ModelType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to a model entry. */ +public class ModelType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/ObjectType.java b/core/src/main/java/dev/quoije/umd/rco/type/ObjectType.java new file mode 100644 index 0000000..e2f3742 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/ObjectType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to an object entry. */ +public class ObjectType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/RefType.java b/core/src/main/java/dev/quoije/umd/rco/type/RefType.java new file mode 100644 index 0000000..c44b8f5 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/RefType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to an arbitrary RCO entry. */ +public class RefType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/TextType.java b/core/src/main/java/dev/quoije/umd/rco/type/TextType.java new file mode 100644 index 0000000..d6e782a --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/TextType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** Reference to a text entry. */ +public class TextType extends BaseReferenceType { +} diff --git a/core/src/main/java/dev/quoije/umd/rco/type/UnknownType.java b/core/src/main/java/dev/quoije/umd/rco/type/UnknownType.java new file mode 100644 index 0000000..ecfe45b --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/type/UnknownType.java @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.quoije.umd.rco.type; + +/** A 32-bit field of unknown semantics. */ +public class UnknownType extends BaseType { +}