Add RCO and VSMX parsers (menu foundation)
- Lzr: RLZ decompressor ported from JPCSP (for compressed RCO data). - RcoFile: parses RCO header, entry tree (labels, VSMX blob, image/ sound/font/object/animation tables, text), and event name table. - RcoEntrySizes: fixed binary sizes of the 26 object and 11 animation record types, needed to locate entries with entrySize=0. - Vsmx: parses VSMX bytecode (code groups + string tables) and disassembles to human-readable opcodes; VsmxCode holds the opcode table. - CLI 'rco' command dumps events, entry tree, and VSMX disassembly. - Validated against Batman Begins EN/FR menus: full scene graph (pages/planes/buttons/groups) and 5585-bytecode scripts decoded.
This commit is contained in:
@@ -9,18 +9,21 @@ import dev.quoije.umd.mps.AudioFramer;
|
||||
import dev.quoije.umd.mps.MpsDemuxer;
|
||||
import dev.quoije.umd.player.PlaybackEngine;
|
||||
import dev.quoije.umd.playlist.Playlist;
|
||||
import dev.quoije.umd.rco.RcoFile;
|
||||
import dev.quoije.umd.rco.vsmx.Vsmx;
|
||||
import dev.quoije.umd.sfo.Sfo;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Command-line front-end for the UMD player library. */
|
||||
public final class Main {
|
||||
|
||||
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>\n umdplayer audio <image.iso> <streamName> [count]\n umdplayer adecode <image.iso> <streamName> [maxPackets]\n umdplayer play <image.iso> <streamName> [seconds]";
|
||||
private static final String USAGE = "usage: umdplayer dump <image.iso>\n umdplayer mps <image.iso> <streamName>\n umdplayer decode <image.iso> <streamName>\n umdplayer audio <image.iso> <streamName> [count]\n umdplayer adecode <image.iso> <streamName> [maxPackets]\n umdplayer play <image.iso> <streamName> [seconds]\n umdplayer rco <image.iso> <rcoPath>";
|
||||
|
||||
private Main() {
|
||||
}
|
||||
@@ -40,6 +43,7 @@ public final class Main {
|
||||
case "audio" -> audio(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 8);
|
||||
case "adecode" -> adecode(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Integer.parseInt(args[3]) : 500);
|
||||
case "play" -> play(requireArg(args, 1), requireArg(args, 2), args.length > 3 ? Double.parseDouble(args[3]) : 5.0);
|
||||
case "rco" -> rco(requireArg(args, 1), requireArg(args, 2));
|
||||
case "help", "--help", "-h" -> {
|
||||
System.out.println(USAGE);
|
||||
yield 0;
|
||||
@@ -384,6 +388,61 @@ public final class Main {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int rco(String imagePath, String rcoPath) throws IOException {
|
||||
try (UmdIsoReader iso = new UmdIsoReader(Path.of(imagePath))) {
|
||||
byte[] data = readFile(iso, rcoPath);
|
||||
if (data == null) {
|
||||
System.err.println("not found: " + rcoPath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
RcoFile rco = new RcoFile(data);
|
||||
System.out.printf("%s: valid=%b compression=%d%n", rcoPath, rco.isValid(), rco.compression());
|
||||
|
||||
System.out.printf("events (%d):%n", rco.events().size());
|
||||
for (Map.Entry<Integer, String> event : rco.events().entrySet()) {
|
||||
System.out.printf(" [%3d] %s%n", event.getKey(), event.getValue());
|
||||
}
|
||||
|
||||
if (rco.mainTable() != null) {
|
||||
System.out.println("entries:");
|
||||
printRcoEntry(rco.mainTable(), 0);
|
||||
}
|
||||
|
||||
byte[] vsmxData = rco.vsmxData();
|
||||
if (vsmxData != null) {
|
||||
Vsmx vsmx = new Vsmx(vsmxData, "menu");
|
||||
System.out.printf("vsmx: %d code groups, %d texts, %d props, %d names%n",
|
||||
vsmx.codes().length, vsmx.texts().length, vsmx.properties().length, vsmx.names().length);
|
||||
List<String> lines = vsmx.disassemble();
|
||||
int shown = Math.min(lines.size(), 100);
|
||||
for (int i = 0; i < shown; i++) {
|
||||
System.out.println(" " + lines.get(i));
|
||||
}
|
||||
if (lines.size() > shown) {
|
||||
System.out.printf(" ... (%d more lines)%n", lines.size() - shown);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void printRcoEntry(RcoFile.Entry entry, int depth) {
|
||||
String indent = " ".repeat(depth);
|
||||
System.out.printf("%s[%s] type=%d label=%s subEntries=%d size=%d%n",
|
||||
indent, entry.tableName(), entry.type,
|
||||
entry.label == null ? "" : "'" + entry.label + "'",
|
||||
entry.numSubEntries, entry.entrySize);
|
||||
if (entry.texts != null) {
|
||||
for (int i = 0; i < entry.texts.length; i++) {
|
||||
System.out.printf("%s text[%d] = '%s'%n", indent, i, entry.texts[i]);
|
||||
}
|
||||
}
|
||||
for (RcoFile.Entry sub : entry.subEntries) {
|
||||
printRcoEntry(sub, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static String hexPrefix(byte[] data, int max) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(data.length, max); i++) {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.rco;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* LZR (RLZ) decompression, used for compressed data inside RCO files.
|
||||
*
|
||||
* <p>Ported from JPCSP's {@code jpcsp.format.rco.LZR}, which is in turn based
|
||||
* on libLZR 0.11 by BenHur.</p>
|
||||
*/
|
||||
public final class Lzr {
|
||||
|
||||
private Lzr() {
|
||||
}
|
||||
|
||||
private static int u8(byte b) {
|
||||
return b & 0xFF;
|
||||
}
|
||||
|
||||
private static long u32(int i) {
|
||||
return i & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
private static final class IntObject {
|
||||
private int value;
|
||||
|
||||
IntObject(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
int get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
void set(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
int incr() {
|
||||
return value++;
|
||||
}
|
||||
|
||||
void sub(int sub) {
|
||||
value -= sub;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BoolObject {
|
||||
private boolean value;
|
||||
|
||||
boolean get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
void set(boolean value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillBuffer(IntObject testMask, IntObject mask, IntObject buffer, byte[] in, IntObject nextIn) {
|
||||
if (testMask.get() >= 0 && testMask.get() <= 0x00FFFFFF) {
|
||||
buffer.set((buffer.get() << 8) + u8(in[nextIn.incr()]));
|
||||
mask.set(testMask.get() << 8);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean nextBit(byte[] buf, int bufPtr1, IntObject number, IntObject testMask, IntObject mask, IntObject buffer, byte[] in, IntObject nextIn) {
|
||||
fillBuffer(testMask, mask, buffer, in, nextIn);
|
||||
int value = (mask.get() >>> 8) * u8(buf[bufPtr1]);
|
||||
if (testMask != mask) {
|
||||
testMask.set(value);
|
||||
}
|
||||
buf[bufPtr1] -= u8(buf[bufPtr1]) >> 3;
|
||||
number.set(number.get() << 1);
|
||||
if (u32(buffer.get()) < u32(value)) {
|
||||
mask.set(value);
|
||||
buf[bufPtr1] += 31;
|
||||
number.incr();
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer.sub(value);
|
||||
mask.sub(value);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int getNumber(int nBits, byte[] buf, int bufPtr, int inc, BoolObject flag, IntObject mask, IntObject buffer, byte[] in, IntObject nextIn) {
|
||||
IntObject number = new IntObject(1);
|
||||
if (nBits >= 3) {
|
||||
nextBit(buf, bufPtr + 3 * inc, number, mask, mask, buffer, in, nextIn);
|
||||
if (nBits >= 4) {
|
||||
nextBit(buf, bufPtr + 3 * inc, number, mask, mask, buffer, in, nextIn);
|
||||
if (nBits >= 5) {
|
||||
fillBuffer(mask, mask, buffer, in, nextIn);
|
||||
for (; nBits >= 5; nBits--) {
|
||||
number.set(number.get() << 1);
|
||||
mask.set(mask.get() >>> 1);
|
||||
if (u32(buffer.get()) < u32(mask.get())) {
|
||||
number.incr();
|
||||
} else {
|
||||
buffer.sub(mask.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flag.set(nextBit(buf, bufPtr, number, mask, mask, buffer, in, nextIn));
|
||||
if (nBits >= 1) {
|
||||
nextBit(buf, bufPtr + inc, number, mask, mask, buffer, in, nextIn);
|
||||
if (nBits >= 2) {
|
||||
nextBit(buf, bufPtr + 2 * inc, number, mask, mask, buffer, in, nextIn);
|
||||
}
|
||||
}
|
||||
return number.get();
|
||||
}
|
||||
|
||||
/** Decompresses {@code in} into {@code out}. Returns bytes written, or -1. */
|
||||
public static int decompress(byte[] out, int outCapacity, byte[] in) {
|
||||
int type = in[0];
|
||||
IntObject buffer = new IntObject((u8(in[1]) << 24) | (u8(in[2]) << 16) | (u8(in[3]) << 8) | u8(in[4]));
|
||||
|
||||
IntObject nextIn = new IntObject(5);
|
||||
int nextOut = 0;
|
||||
int outEnd = outCapacity;
|
||||
|
||||
if (type < 0) {
|
||||
int seqEnd = nextOut + buffer.get();
|
||||
if (seqEnd > outEnd) {
|
||||
return -1;
|
||||
}
|
||||
while (nextOut < seqEnd) {
|
||||
out[nextOut++] = in[nextIn.incr()];
|
||||
}
|
||||
return nextOut;
|
||||
}
|
||||
|
||||
byte[] buf = new byte[2800];
|
||||
Arrays.fill(buf, (byte) 0x80);
|
||||
int bufOff = 0;
|
||||
|
||||
IntObject mask = new IntObject(0xFFFFFFFF);
|
||||
IntObject testMask = new IntObject(0);
|
||||
int lastChar = 0;
|
||||
|
||||
while (true) {
|
||||
int bufPtr1 = bufOff + 2488;
|
||||
if (!nextBit(buf, bufPtr1, new IntObject(0), mask, mask, buffer, in, nextIn)) {
|
||||
if (bufOff > 0) {
|
||||
bufOff--;
|
||||
}
|
||||
if (nextOut == outEnd) {
|
||||
return -1;
|
||||
}
|
||||
bufPtr1 = (((((nextOut & 0x07) << 8) + lastChar) >> type) & 0x07) * 0xFF - 0x01;
|
||||
IntObject j = new IntObject(1);
|
||||
while (j.get() <= 0xFF) {
|
||||
nextBit(buf, bufPtr1 + j.get(), j, mask, mask, buffer, in, nextIn);
|
||||
}
|
||||
out[nextOut++] = (byte) j.get();
|
||||
} else {
|
||||
testMask.set(mask.get());
|
||||
int nBits = -1;
|
||||
BoolObject flag = new BoolObject();
|
||||
do {
|
||||
bufPtr1 += 8;
|
||||
flag.set(nextBit(buf, bufPtr1, new IntObject(0), testMask, mask, buffer, in, nextIn));
|
||||
if (flag.get()) {
|
||||
nBits++;
|
||||
}
|
||||
} while (flag.get() && nBits < 6);
|
||||
|
||||
int bufPtr2 = nBits + 2033;
|
||||
int j = 64;
|
||||
int seqLen;
|
||||
if (flag.get() || nBits >= 0) {
|
||||
bufPtr1 = (nBits << 5) + (((nextOut << nBits) & 0x03) << 3) + bufOff + 2552;
|
||||
seqLen = getNumber(nBits, buf, bufPtr1, 8, flag, mask, buffer, in, nextIn);
|
||||
if (seqLen == 0xFF) {
|
||||
return nextOut;
|
||||
}
|
||||
if (flag.get() || nBits > 0) {
|
||||
bufPtr2 += 56;
|
||||
j = 352;
|
||||
}
|
||||
} else {
|
||||
seqLen = 1;
|
||||
}
|
||||
|
||||
IntObject i = new IntObject(1);
|
||||
do {
|
||||
nBits = (i.get() << 4) - j;
|
||||
flag.set(nextBit(buf, bufPtr2 + (i.get() << 3), i, mask, mask, buffer, in, nextIn));
|
||||
} while (nBits < 0);
|
||||
|
||||
int seqOff;
|
||||
if (flag.get() || nBits > 0) {
|
||||
if (!flag.get()) {
|
||||
nBits -= 8;
|
||||
}
|
||||
seqOff = getNumber(nBits / 8, buf, nBits + 2344, 1, flag, mask, buffer, in, nextIn);
|
||||
} else {
|
||||
seqOff = 1;
|
||||
}
|
||||
|
||||
int nextSeq = nextOut - seqOff;
|
||||
if (nextSeq < 0) {
|
||||
return -1;
|
||||
}
|
||||
int seqEnd = nextOut + seqLen + 1;
|
||||
if (seqEnd > outEnd) {
|
||||
return -1;
|
||||
}
|
||||
bufOff = ((seqEnd + 1) & 0x01) + 0x06;
|
||||
do {
|
||||
out[nextOut++] = out[nextSeq++];
|
||||
} while (nextOut < seqEnd);
|
||||
}
|
||||
lastChar = u8(out[nextOut - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.rco;
|
||||
|
||||
/**
|
||||
* Fixed binary sizes of RCO object and animation records, used to locate
|
||||
* entries whose {@code entrySize} field is zero. Sizes are the sum of each
|
||||
* record's typed fields: scalar types (int/float/unknown) are 4 bytes and
|
||||
* reference types (event/image/text/object/model/ref) are 8 bytes.
|
||||
*
|
||||
* <p>Ported from JPCSP's {@code ObjectFactory} / {@code AnimFactory} object
|
||||
* model field layouts.</p>
|
||||
*/
|
||||
public final class RcoEntrySizes {
|
||||
|
||||
/** Object record sizes indexed by object type (1..26). */
|
||||
private static final int[] OBJECT = {
|
||||
0, // type 0 unused
|
||||
36, // 1 PageObject
|
||||
76, // 2 PlaneObject
|
||||
156, // 3 ButtonObject
|
||||
108, // 4 XMenuObject
|
||||
20, // 5 XMListObject
|
||||
124, // 6 XListObject
|
||||
96, // 7 ProgressObject
|
||||
116, // 8 ScrollObject
|
||||
172, // 9 MListObject
|
||||
24, // 10 MItemObject
|
||||
0, // 11 unknown
|
||||
24, // 12 XItemObject
|
||||
196, // 13 TextObject
|
||||
72, // 14 ModelObject
|
||||
144, // 15 SpinObject
|
||||
8, // 16 ActionObject
|
||||
160, // 17 ItemSpinObject
|
||||
64, // 18 GroupObject
|
||||
132, // 19 LListObject
|
||||
24, // 20 LItemObject
|
||||
168, // 21 EditObject
|
||||
176, // 22 ClockObject
|
||||
100, // 23 IListObject
|
||||
16, // 24 IItemObject
|
||||
88, // 25 IconObject
|
||||
132, // 26 UButtonObject
|
||||
};
|
||||
|
||||
/** Animation record sizes indexed by animation type (1..11). */
|
||||
private static final int[] ANIM = {
|
||||
0, // type 0 unused
|
||||
0, // 1 Anim
|
||||
28, // 2 MoveToAnim
|
||||
32, // 3 RecolourAnim
|
||||
28, // 4 RotateAnim
|
||||
28, // 5 ResizeAnim
|
||||
20, // 6 FadeAnim
|
||||
4, // 7 DelayAnim
|
||||
8, // 8 FireEventAnim
|
||||
4, // 9 LockAnim
|
||||
4, // 10 UnlockAnim
|
||||
32, // 11 SlideOutAnim
|
||||
};
|
||||
|
||||
private RcoEntrySizes() {
|
||||
}
|
||||
|
||||
/** Size in bytes of an object record of the given type, or 0 if unknown. */
|
||||
public static int objectSize(int type) {
|
||||
return type >= 0 && type < OBJECT.length ? OBJECT[type] : 0;
|
||||
}
|
||||
|
||||
/** Size in bytes of an animation record of the given type, or 0 if unknown. */
|
||||
public static int animSize(int type) {
|
||||
return type >= 0 && type < ANIM.length ? ANIM[type] : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.rco;
|
||||
|
||||
import dev.quoije.umd.rco.vsmx.Vsmx;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Parser for PSP RCO resource files (UMD video menus).
|
||||
*
|
||||
* <p>Parses the RCO header, the entry tree (labels, text, images, sounds,
|
||||
* fonts, objects, animations, and the VSMX script), and the event name table.
|
||||
* This port focuses on the structural data needed to drive the menu; full
|
||||
* object/animation instantiation is layered on top in later stages.</p>
|
||||
*/
|
||||
public final class RcoFile {
|
||||
|
||||
public static final int RCO_MAGIC = 0x00505246;
|
||||
public static final int RCO_NULL_PTR = 0xFFFFFFFF;
|
||||
|
||||
public static final int TABLE_MAIN = 1;
|
||||
public static final int TABLE_VSMX = 2;
|
||||
public static final int TABLE_TEXT = 3;
|
||||
public static final int TABLE_IMG = 4;
|
||||
public static final int TABLE_MODEL = 5;
|
||||
public static final int TABLE_SOUND = 6;
|
||||
public static final int TABLE_FONT = 7;
|
||||
public static final int TABLE_OBJ = 8;
|
||||
public static final int TABLE_ANIM = 9;
|
||||
|
||||
public static final int COMPRESSION_NONE = 0;
|
||||
public static final int COMPRESSION_ZLIB = 1;
|
||||
public static final int COMPRESSION_RLZ = 2;
|
||||
|
||||
private static final int ENTRY_SIZE = 40;
|
||||
|
||||
private static final int RCO_HEADER_SIZE = 164;
|
||||
|
||||
/** One entry (node) in the RCO table tree. */
|
||||
public static final class Entry {
|
||||
public int type;
|
||||
public int id;
|
||||
public int labelOffset;
|
||||
public String label;
|
||||
public int entrySize;
|
||||
public int numSubEntries;
|
||||
public int nextEntryOffset;
|
||||
public int prevEntryOffset;
|
||||
public Entry parent;
|
||||
public final List<Entry> subEntries = new ArrayList<>();
|
||||
public byte[] data;
|
||||
public String[] texts;
|
||||
|
||||
public String tableName() {
|
||||
return RcoFile.tableName(id);
|
||||
}
|
||||
}
|
||||
|
||||
private final boolean valid;
|
||||
private int compression;
|
||||
private final Entry mainTable;
|
||||
private final byte[] vsmxData;
|
||||
private final Map<Integer, String> events = new LinkedHashMap<>();
|
||||
|
||||
private final byte[] buffer;
|
||||
private int offset;
|
||||
private int pTextData;
|
||||
private int lTextData;
|
||||
private int pLabelData;
|
||||
private int lLabelData;
|
||||
private int pMainTable;
|
||||
private int pEventData;
|
||||
private int lEventData;
|
||||
private final Map<Integer, Entry> entries = new HashMap<>();
|
||||
|
||||
public RcoFile(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
|
||||
boolean ok;
|
||||
int comp = COMPRESSION_NONE;
|
||||
Entry main = null;
|
||||
byte[] vsmx = null;
|
||||
try {
|
||||
readHeader();
|
||||
comp = compression;
|
||||
readEvents();
|
||||
main = readEntry(pMainTable);
|
||||
vsmx = entries.values().stream()
|
||||
.filter(e -> e.id == TABLE_VSMX && e.data != null)
|
||||
.map(e -> e.data)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
ok = true;
|
||||
} catch (RuntimeException e) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
this.valid = ok;
|
||||
this.compression = comp;
|
||||
this.mainTable = main;
|
||||
this.vsmxData = vsmx;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public int compression() {
|
||||
return compression;
|
||||
}
|
||||
|
||||
public Entry mainTable() {
|
||||
return mainTable;
|
||||
}
|
||||
|
||||
public byte[] vsmxData() {
|
||||
return vsmxData;
|
||||
}
|
||||
|
||||
public Map<Integer, String> events() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public static String tableName(int id) {
|
||||
return switch (id) {
|
||||
case TABLE_MAIN -> "MAIN";
|
||||
case TABLE_VSMX -> "VSMX";
|
||||
case TABLE_TEXT -> "TEXT";
|
||||
case TABLE_IMG -> "IMG";
|
||||
case TABLE_MODEL -> "MODEL";
|
||||
case TABLE_SOUND -> "SOUND";
|
||||
case TABLE_FONT -> "FONT";
|
||||
case TABLE_OBJ -> "OBJ";
|
||||
case TABLE_ANIM -> "ANIM";
|
||||
default -> "0x" + Integer.toHexString(id);
|
||||
};
|
||||
}
|
||||
|
||||
private void readHeader() {
|
||||
int magic = Integer.reverseBytes(read32());
|
||||
if (magic != RCO_MAGIC) {
|
||||
throw new IllegalStateException(String.format("Invalid RCO magic 0x%08X", magic));
|
||||
}
|
||||
read32(); // version
|
||||
skip(4); // null
|
||||
compression = read32();
|
||||
pMainTable = read32();
|
||||
int pVSMXTable = read32();
|
||||
int pTextTable = read32();
|
||||
int pSoundTable = read32();
|
||||
int pModelTable = read32();
|
||||
int pImgTable = read32();
|
||||
skip(4); // pUnknown
|
||||
int pFontTable = read32();
|
||||
int pObjTable = read32();
|
||||
int pAnimTable = read32();
|
||||
pTextData = read32();
|
||||
lTextData = read32();
|
||||
pLabelData = read32();
|
||||
lLabelData = read32();
|
||||
pEventData = read32();
|
||||
lEventData = read32();
|
||||
|
||||
// Remaining pointer/data tables (18 ints) plus 3 trailing 0xFFFFFFFF.
|
||||
skip(21 * 4);
|
||||
}
|
||||
|
||||
private void readEvents() {
|
||||
if (pEventData != RCO_NULL_PTR && lEventData > 0) {
|
||||
seek(pEventData);
|
||||
while (tell() < pEventData + lEventData) {
|
||||
int index = tell() - pEventData;
|
||||
String s = readString();
|
||||
if (s != null && !s.isEmpty()) {
|
||||
events.put(index, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Entry readEntry(int entryOffset) {
|
||||
if (entryOffset == RCO_NULL_PTR) {
|
||||
return null;
|
||||
}
|
||||
if (entryOffset < 0 || entryOffset >= buffer.length) {
|
||||
throw new IllegalStateException("entry offset out of range: " + entryOffset);
|
||||
}
|
||||
seek(entryOffset);
|
||||
Entry entry = new Entry();
|
||||
entries.put(entryOffset, entry);
|
||||
|
||||
entry.type = read8();
|
||||
entry.id = read8();
|
||||
skip(2);
|
||||
entry.labelOffset = read32();
|
||||
int eHeadSize = read32();
|
||||
entry.entrySize = read32();
|
||||
entry.numSubEntries = read32();
|
||||
int nextEntryOffset = read32();
|
||||
int prevEntryOffset = read32();
|
||||
int parentTblOffset = read32();
|
||||
skip(8);
|
||||
|
||||
entry.parent = entries.get(entryOffset - parentTblOffset);
|
||||
if (entry.labelOffset != RCO_NULL_PTR) {
|
||||
entry.label = readLabel(entry.labelOffset);
|
||||
}
|
||||
|
||||
switch (entry.id) {
|
||||
case TABLE_VSMX -> {
|
||||
if (entry.type == 1) {
|
||||
int offsetVSMX = read32();
|
||||
int lengthVSMX = read32();
|
||||
skip(offsetVSMX);
|
||||
entry.data = readBytes(lengthVSMX);
|
||||
skip(alignUp(lengthVSMX, 4) - lengthVSMX);
|
||||
}
|
||||
}
|
||||
case TABLE_IMG, TABLE_MODEL -> {
|
||||
if (entry.type == 1) {
|
||||
skip(2); // format
|
||||
int comp = read16();
|
||||
int sizePacked = read32();
|
||||
skip(4); // offset
|
||||
if (comp != COMPRESSION_NONE) {
|
||||
skip(4); // sizeUnpacked
|
||||
}
|
||||
}
|
||||
}
|
||||
case TABLE_SOUND -> {
|
||||
if (entry.type == 1) {
|
||||
skip(2); // format
|
||||
int channels = read16();
|
||||
skip(4); // sizeTotal
|
||||
skip(4); // offset
|
||||
skip(8 * Math.max(channels, 2)); // per-channel size/offset pairs
|
||||
}
|
||||
}
|
||||
case TABLE_FONT -> {
|
||||
if (entry.type == 1) {
|
||||
skip(2); // format
|
||||
skip(2); // compression
|
||||
skip(4); // unknown1
|
||||
skip(4); // unknown2
|
||||
}
|
||||
}
|
||||
case TABLE_OBJ -> {
|
||||
int dataLength = entryDataLength(entry, RcoEntrySizes.objectSize(entry.type));
|
||||
if (dataLength > 0) {
|
||||
entry.data = readBytes(dataLength);
|
||||
}
|
||||
}
|
||||
case TABLE_ANIM -> {
|
||||
int dataLength = entryDataLength(entry, RcoEntrySizes.animSize(entry.type));
|
||||
if (dataLength > 0) {
|
||||
entry.data = readBytes(dataLength);
|
||||
}
|
||||
}
|
||||
case TABLE_TEXT -> {
|
||||
if (entry.type == 1) {
|
||||
int lang = read16();
|
||||
skip(2); // format
|
||||
int numIndexes = read32();
|
||||
entry.texts = new String[numIndexes];
|
||||
for (int i = 0; i < numIndexes; i++) {
|
||||
skip(4); // labelOffset
|
||||
int length = read32();
|
||||
int textOffset = read32();
|
||||
entry.texts[i] = readText(textOffset, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// unknown table id; leave data unparsed
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < entry.numSubEntries; i++) {
|
||||
Entry sub = readEntry(tell());
|
||||
if (sub != null) {
|
||||
entry.subEntries.add(sub);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private String readLabel(int labelOffset) {
|
||||
int current = tell();
|
||||
seek(pLabelData + labelOffset);
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int max = lLabelData - labelOffset; max > 0; max--) {
|
||||
int b = read8();
|
||||
if (b == 0) {
|
||||
break;
|
||||
}
|
||||
s.append((char) b);
|
||||
}
|
||||
seek(current);
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
private String readText(int textOffset, int length) {
|
||||
if (textOffset == RCO_NULL_PTR || length <= 0) {
|
||||
return "";
|
||||
}
|
||||
int current = tell();
|
||||
seek(pTextData + textOffset);
|
||||
byte[] bytes = readBytes(length);
|
||||
seek(current);
|
||||
|
||||
int len = length;
|
||||
if (len >= 2 && bytes[len - 1] == 0 && bytes[len - 2] == 0) {
|
||||
len -= 2;
|
||||
}
|
||||
return new String(bytes, 0, len, StandardCharsets.UTF_16LE);
|
||||
}
|
||||
|
||||
private String readString() {
|
||||
StringBuilder s = new StringBuilder();
|
||||
while (true) {
|
||||
int b = read8();
|
||||
if (b == 0) {
|
||||
break;
|
||||
}
|
||||
s.append((char) b);
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
private int entryDataLength(Entry entry, int computedSize) {
|
||||
if (entry.entrySize > ENTRY_SIZE) {
|
||||
return entry.entrySize - ENTRY_SIZE;
|
||||
}
|
||||
if (entry.entrySize == 0) {
|
||||
return computedSize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int read8() {
|
||||
return buffer[offset++] & 0xFF;
|
||||
}
|
||||
|
||||
private int read16() {
|
||||
return read8() | (read8() << 8);
|
||||
}
|
||||
|
||||
private int read32() {
|
||||
return read16() | (read16() << 16);
|
||||
}
|
||||
|
||||
private byte[] readBytes(int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
System.arraycopy(buffer, offset, bytes, 0, length);
|
||||
offset += length;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void skip(int n) {
|
||||
offset += n;
|
||||
}
|
||||
|
||||
private void seek(int offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
private int tell() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static int alignUp(int value, int alignment) {
|
||||
return (value + alignment - 1) & ~(alignment - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.rco.vsmx;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Parser for the VSMX bytecode blob embedded in an RCO file.
|
||||
*
|
||||
* <p>A VSMX file holds a compiled JavaScript-like script: a code section of
|
||||
* 8-byte groups ({@code id} + {@code value}), plus string tables for text
|
||||
* literals, property names, and variable names.</p>
|
||||
*/
|
||||
public final class Vsmx {
|
||||
|
||||
private static final int VSMX_SIGNATURE = 0x584D5356; // "VSMX"
|
||||
private static final int VSMX_VERSION = 0x00010000;
|
||||
|
||||
private static final Charset UTF16_LE = StandardCharsets.UTF_16LE;
|
||||
private static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
|
||||
|
||||
/** One 8-byte code group: an opcode id plus an operand value. */
|
||||
public record Group(int id, int value) {
|
||||
public int opcode() {
|
||||
return id & 0xFF;
|
||||
}
|
||||
|
||||
public float floatValue() {
|
||||
return Float.intBitsToFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final Group[] codes;
|
||||
private final String[] texts;
|
||||
private final String[] properties;
|
||||
private final String[] names;
|
||||
|
||||
public Vsmx(byte[] buffer, String name) {
|
||||
this.name = name;
|
||||
|
||||
ByteCursor c = new ByteCursor(buffer);
|
||||
int sig = c.read32();
|
||||
int ver = c.read32();
|
||||
int codeOffset = c.read32();
|
||||
int codeLength = c.read32();
|
||||
int textOffset = c.read32();
|
||||
int textLength = c.read32();
|
||||
int textEntries = c.read32();
|
||||
int propOffset = c.read32();
|
||||
int propLength = c.read32();
|
||||
int propEntries = c.read32();
|
||||
int namesOffset = c.read32();
|
||||
int namesLength = c.read32();
|
||||
int namesEntries = c.read32();
|
||||
|
||||
if (sig != VSMX_SIGNATURE) {
|
||||
throw new IllegalArgumentException(String.format("Invalid VSMX signature 0x%08X", sig));
|
||||
}
|
||||
if (ver != VSMX_VERSION) {
|
||||
throw new IllegalArgumentException(String.format("Invalid VSMX version 0x%08X", ver));
|
||||
}
|
||||
|
||||
if (codeLength % 8 != 0) {
|
||||
throw new IllegalArgumentException("VSMX code length not aligned to 8 bytes: " + codeLength);
|
||||
}
|
||||
|
||||
codes = new Group[codeLength / 8];
|
||||
c.seek(codeOffset);
|
||||
for (int i = 0; i < codes.length; i++) {
|
||||
codes[i] = new Group(c.read32(), c.read32());
|
||||
}
|
||||
|
||||
texts = readStrings(buffer, textOffset, textLength, textEntries, UTF16_LE, 2);
|
||||
properties = readStrings(buffer, propOffset, propLength, propEntries, UTF16_LE, 2);
|
||||
names = readStrings(buffer, namesOffset, namesLength, namesEntries, ISO_8859_1, 1);
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Group[] codes() {
|
||||
return codes;
|
||||
}
|
||||
|
||||
public String[] texts() {
|
||||
return texts;
|
||||
}
|
||||
|
||||
public String[] properties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public String[] names() {
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Disassembles the code section into one human-readable line per group. */
|
||||
public List<String> disassemble() {
|
||||
List<String> lines = new ArrayList<>(codes.length);
|
||||
for (int i = 0; i < codes.length; i++) {
|
||||
Group code = codes[i];
|
||||
int opcode = code.opcode();
|
||||
String mnemonic = opcode >= 0 && opcode < VsmxCode.MNEMONICS.length
|
||||
? VsmxCode.MNEMONICS[opcode]
|
||||
: "UNKNOWN_" + Integer.toHexString(opcode);
|
||||
|
||||
String operand = switch (opcode) {
|
||||
case VsmxCode.VID_CONST_BOOL -> code.value() == 1 ? "true" : code.value() == 0 ? "false" : "0x" + Integer.toHexString(code.value());
|
||||
case VsmxCode.VID_CONST_INT, VsmxCode.VID_DEBUG_LINE -> Integer.toString(code.value());
|
||||
case VsmxCode.VID_CONST_FLOAT -> Float.toString(code.floatValue());
|
||||
case VsmxCode.VID_CONST_STRING, VsmxCode.VID_DEBUG_FILE -> "'" + texts[code.value()] + "'";
|
||||
case VsmxCode.VID_VARIABLE -> names[code.value()];
|
||||
case VsmxCode.VID_PROPERTY, VsmxCode.VID_METHOD, VsmxCode.VID_SET_ATTR, VsmxCode.VID_UNSET, VsmxCode.VID_OBJ_ADD_ATTR ->
|
||||
properties[code.value()];
|
||||
case VsmxCode.VID_FUNCTION -> {
|
||||
int args = (code.id() >> 8) & 0xFF;
|
||||
int localVars = (code.id() >> 24) & 0xFF;
|
||||
yield "args=" + args + ", localVars=" + localVars + ", startLine=" + code.value();
|
||||
}
|
||||
case VsmxCode.VID_UNNAMED_VAR -> Integer.toString(code.value());
|
||||
case VsmxCode.VID_JUMP, VsmxCode.VID_JUMP_TRUE, VsmxCode.VID_JUMP_FALSE -> "line=" + code.value();
|
||||
case VsmxCode.VID_CALL_FUNC, VsmxCode.VID_CALL_METHOD, VsmxCode.VID_CALL_NEW -> "args=" + code.value();
|
||||
case VsmxCode.VID_MAKE_FLOAT_ARRAY -> "items=" + code.value();
|
||||
default -> code.value() == 0 ? "" : "0x" + Integer.toHexString(code.value());
|
||||
};
|
||||
|
||||
lines.add(String.format("%5d: %-22s %s", i, mnemonic, operand));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static String[] readStrings(byte[] buffer, int offset, int length, int entries, Charset charset, int bytesPerChar) {
|
||||
String[] strings = new String[entries];
|
||||
int stringIndex = 0;
|
||||
int stringStart = 0;
|
||||
for (int i = 0; i < length && stringIndex < entries; i += bytesPerChar) {
|
||||
boolean zero = true;
|
||||
for (int j = 0; j < bytesPerChar; j++) {
|
||||
if (buffer[offset + i + j] != 0) {
|
||||
zero = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (zero) {
|
||||
strings[stringIndex++] = new String(buffer, offset + stringStart, i - stringStart, charset);
|
||||
stringStart = i + bytesPerChar;
|
||||
}
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
private static final class ByteCursor {
|
||||
private final byte[] buffer;
|
||||
private int offset;
|
||||
|
||||
ByteCursor(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
int read8() {
|
||||
return buffer[offset++] & 0xFF;
|
||||
}
|
||||
|
||||
int read16() {
|
||||
return read8() | (read8() << 8);
|
||||
}
|
||||
|
||||
int read32() {
|
||||
return read16() | (read16() << 16);
|
||||
}
|
||||
|
||||
void seek(int offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.rco.vsmx;
|
||||
|
||||
/** VSMX bytecode opcode constants and mnemonic names (ported from JPCSP). */
|
||||
public final class VsmxCode {
|
||||
|
||||
public static final int VID_NOTHING = 0x00;
|
||||
public static final int VID_OPERATOR_ASSIGN = 0x01;
|
||||
public static final int VID_OPERATOR_ADD = 0x02;
|
||||
public static final int VID_OPERATOR_SUBTRACT = 0x03;
|
||||
public static final int VID_OPERATOR_MULTIPLY = 0x04;
|
||||
public static final int VID_OPERATOR_DIVIDE = 0x05;
|
||||
public static final int VID_OPERATOR_MOD = 0x06;
|
||||
public static final int VID_OPERATOR_POSITIVE = 0x07;
|
||||
public static final int VID_OPERATOR_NEGATE = 0x08;
|
||||
public static final int VID_OPERATOR_NOT = 0x09;
|
||||
public static final int VID_P_INCREMENT = 0x0A;
|
||||
public static final int VID_P_DECREMENT = 0x0B;
|
||||
public static final int VID_INCREMENT = 0x0C;
|
||||
public static final int VID_DECREMENT = 0x0D;
|
||||
public static final int VID_OPERATOR_EQUAL = 0x0E;
|
||||
public static final int VID_OPERATOR_NOT_EQUAL = 0x0F;
|
||||
public static final int VID_OPERATOR_IDENTITY = 0x10;
|
||||
public static final int VID_OPERATOR_NON_IDENTITY = 0x11;
|
||||
public static final int VID_OPERATOR_LT = 0x12;
|
||||
public static final int VID_OPERATOR_LTE = 0x13;
|
||||
public static final int VID_OPERATOR_GTE = 0x14;
|
||||
public static final int VID_OPERATOR_GT = 0x15;
|
||||
public static final int VID_OPERATOR_INSTANCEOF = 0x16;
|
||||
public static final int VID_OPERATOR_IN = 0x17;
|
||||
public static final int VID_OPERATOR_TYPEOF = 0x18;
|
||||
public static final int VID_OPERATOR_B_AND = 0x19;
|
||||
public static final int VID_OPERATOR_B_XOR = 0x1A;
|
||||
public static final int VID_OPERATOR_B_OR = 0x1B;
|
||||
public static final int VID_OPERATOR_B_NOT = 0x1C;
|
||||
public static final int VID_OPERATOR_LSHIFT = 0x1D;
|
||||
public static final int VID_OPERATOR_RSHIFT = 0x1E;
|
||||
public static final int VID_OPERATOR_URSHIFT = 0x1F;
|
||||
public static final int VID_STACK_COPY = 0x20;
|
||||
public static final int VID_STACK_SWAP = 0x21;
|
||||
public static final int VID_END_STMT = 0x22;
|
||||
public static final int VID_CONST_NULL = 0x23;
|
||||
public static final int VID_CONST_EMPTYARRAY = 0x24;
|
||||
public static final int VID_CONST_BOOL = 0x25;
|
||||
public static final int VID_CONST_INT = 0x26;
|
||||
public static final int VID_CONST_FLOAT = 0x27;
|
||||
public static final int VID_CONST_STRING = 0x28;
|
||||
public static final int VID_CONST_OBJECT = 0x29;
|
||||
public static final int VID_FUNCTION = 0x2A;
|
||||
public static final int VID_ARRAY = 0x2B;
|
||||
public static final int VID_THIS = 0x2C;
|
||||
public static final int VID_UNNAMED_VAR = 0x2D;
|
||||
public static final int VID_VARIABLE = 0x2E;
|
||||
public static final int VID_PROPERTY = 0x2F;
|
||||
public static final int VID_METHOD = 0x30;
|
||||
public static final int VID_SET_ATTR = 0x31;
|
||||
public static final int VID_UNSET = 0x32;
|
||||
public static final int VID_OBJ_ADD_ATTR = 0x33;
|
||||
public static final int VID_ARRAY_INDEX = 0x34;
|
||||
public static final int VID_ARRAY_INDEX_KEEP_OBJ = 0x35;
|
||||
public static final int VID_ARRAY_INDEX_ASSIGN = 0x36;
|
||||
public static final int VID_ARRAY_DELETE = 0x37;
|
||||
public static final int VID_ARRAY_PUSH = 0x38;
|
||||
public static final int VID_JUMP = 0x39;
|
||||
public static final int VID_JUMP_TRUE = 0x3A;
|
||||
public static final int VID_JUMP_FALSE = 0x3B;
|
||||
public static final int VID_CALL_FUNC = 0x3C;
|
||||
public static final int VID_CALL_METHOD = 0x3D;
|
||||
public static final int VID_CALL_NEW = 0x3E;
|
||||
public static final int VID_RETURN = 0x3F;
|
||||
public static final int VID_THROW = 0x40;
|
||||
public static final int VID_TRY_BLOCK_IN = 0x41;
|
||||
public static final int VID_TRY_BLOCK_OUT = 0x42;
|
||||
public static final int VID_CATCH_FINALLY_BLOCK_IN = 0x43;
|
||||
public static final int VID_CATCH_FINALLY_BLOCK_OUT = 0x44;
|
||||
public static final int VID_END = 0x45;
|
||||
public static final int VID_DEBUG_FILE = 0x46;
|
||||
public static final int VID_DEBUG_LINE = 0x47;
|
||||
public static final int VID_MAKE_FLOAT_ARRAY = 0x49;
|
||||
|
||||
/** Mnemonics indexed by opcode (0x00..0x49). */
|
||||
public static final String[] MNEMONICS = {
|
||||
"UNKNOWN_0",
|
||||
"ASSIGN",
|
||||
"ADD",
|
||||
"SUBTRACT",
|
||||
"MULTIPLY",
|
||||
"DIVIDE",
|
||||
"MODULUS",
|
||||
"POSITIVE",
|
||||
"NEGATE",
|
||||
"NOT",
|
||||
"PRE_INCREMENT",
|
||||
"PRE_DECREMENT",
|
||||
"INCREMENT",
|
||||
"DECREMENT",
|
||||
"TEST_EQUAL",
|
||||
"TEST_NOT_EQUAL",
|
||||
"TEST_IDENTITY",
|
||||
"TEST_NON_IDENTITY",
|
||||
"TEST_LESS_THAN",
|
||||
"TEST_LESS_EQUAL_THAN",
|
||||
"TEST_MORE_EQUAL_THAN",
|
||||
"TEST_MORE_THAN",
|
||||
"INSTANCEOF",
|
||||
"IN",
|
||||
"TYPEOF",
|
||||
"BINARY_AND",
|
||||
"BINARY_XOR",
|
||||
"BINARY_OR",
|
||||
"BINARY_NOT",
|
||||
"LSHIFT",
|
||||
"RSHIFT",
|
||||
"UNSIGNED_RSHIFT",
|
||||
"STACK_COPY",
|
||||
"STACK_SWAP",
|
||||
"END_STATEMENT",
|
||||
"CONST_NULL",
|
||||
"CONST_EMPTY_ARRAY",
|
||||
"CONST_BOOL",
|
||||
"CONST_INT",
|
||||
"CONST_FLOAT",
|
||||
"CONST_STRING",
|
||||
"CONST_OBJECT",
|
||||
"FUNCTION",
|
||||
"CONST_ARRAY",
|
||||
"THIS_OBJECT",
|
||||
"UNNAMED_VARIABLE",
|
||||
"NAME",
|
||||
"PROPERTY",
|
||||
"METHOD",
|
||||
"SET",
|
||||
"UNSET",
|
||||
"OBJECT_ADD_ATTRIBUTE",
|
||||
"ARRAY_INDEX",
|
||||
"ARRAY_INDEX_KEEP_OBJ",
|
||||
"ARRAY_INDEX_ASSIGN",
|
||||
"ARRAY_DELETE",
|
||||
"ARRAY_PUSH",
|
||||
"JUMP",
|
||||
"JUMP_IF_TRUE",
|
||||
"JUMP_IF_FALSE",
|
||||
"CALL_FUNCTION",
|
||||
"CALL_METHOD",
|
||||
"CALL_NEW",
|
||||
"RETURN",
|
||||
"THROW",
|
||||
"TRY_BLOCK_IN",
|
||||
"TRY_BLOCK_OUT",
|
||||
"CATCH_FINALLY_BLOCK_IN",
|
||||
"CATCH_FINALLY_BLOCK_OUT",
|
||||
"END_SCRIPT",
|
||||
"DEBUG_FILE",
|
||||
"DEBUG_LINE",
|
||||
"UNKNOWN_48",
|
||||
"MAKE_FLOAT_ARRAY",
|
||||
};
|
||||
|
||||
private VsmxCode() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user