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++) {
|
||||
|
||||
Reference in New Issue
Block a user