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:
2026-09-15 22:34:18 -04:00
parent 3768e50603
commit d3e3cbd017
6 changed files with 1076 additions and 1 deletions
+60 -1
View File
@@ -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++) {