Update Rex-EMoolator version

This commit is contained in:
Patryk Gensch
2026-08-20 21:40:10 +02:00
parent fd21d8c9a6
commit aaf2073ff7
6 changed files with 75 additions and 29 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ tasks.register('verifyCoreVersion') {
def submodule = file('vendor/Rex-EMoolator') def submodule = file('vendor/Rex-EMoolator')
doLast { doLast {
if (usingExternal.get()) { if (usingExternal) {
logger.lifecycle('verifyCoreVersion: pominięte (-PemulatorPath, kopia robocza)') logger.lifecycle('verifyCoreVersion: pominięte (-PemulatorPath, kopia robocza)')
return return
} }
+1 -1
View File
@@ -1,7 +1,7 @@
# Wersja :core, na którą przypięty jest submoduł vendor/Rex-EMoolator. # Wersja :core, na którą przypięty jest submoduł vendor/Rex-EMoolator.
# Zadanie verifyCoreVersion pilnuje, żeby ta wartość zgadzała się z `git describe` # Zadanie verifyCoreVersion pilnuje, żeby ta wartość zgadzała się z `git describe`
# w submodule — inaczej łatwo zbumpować jedno i zapomnieć o drugim. # w submodule — inaczej łatwo zbumpować jedno i zapomnieć o drugim.
coreVersion=v0.4.3 coreVersion=v0.5.1
org.gradle.jvmargs=-Xmx2g org.gradle.jvmargs=-Xmx2g
org.gradle.parallel=true org.gradle.parallel=true
@@ -101,7 +101,7 @@ public final class Main {
try (Database db = Database.open(dataDir); try (Database db = Database.open(dataDir);
Statement st = db.connection().createStatement(); Statement st = db.connection().createStatement();
ResultSet rs = st.executeQuery(""" ResultSet rs = st.executeQuery("""
SELECT c.display_name, c.container, c.status, c.file_count, SELECT c.display_name, c.container, c.fs_type, c.status, c.file_count,
g.root_path, g.root_path,
COALESCE(g.engine_override, g.engine_detected) AS engine, COALESCE(g.engine_override, g.engine_detected) AS engine,
MAX(CASE WHEN d.field = 'edition' THEN d.value END) AS edition, MAX(CASE WHEN d.field = 'edition' THEN d.value END) AS edition,
@@ -114,12 +114,13 @@ public final class Main {
ORDER BY family, edition, c.display_name, g.root_path ORDER BY family, edition, c.display_name, g.root_path
""")) { """)) {
System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n", System.out.printf("%-40s %-8s %-10s %-10s %-13s %-22s %s%n",
"KOPIA", "STATUS", "KORZEŃ", "SILNIK", "RODZINA", "WYDANIE"); "KOPIA", "FS", "STATUS", "KORZEŃ", "SILNIK", "RODZINA", "WYDANIE");
while (rs.next()) { while (rs.next()) {
String root = rs.getString("root_path"); String root = rs.getString("root_path");
System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n", System.out.printf("%-40s %-8s %-10s %-10s %-13s %-22s %s%n",
truncate(rs.getString("display_name"), 40), truncate(rs.getString("display_name"), 40),
nvl(rs.getString("fs_type")),
rs.getString("status"), rs.getString("status"),
root == null ? "" : (root.isEmpty() ? "/" : root + "/"), root == null ? "" : (root.isEmpty() ? "/" : root + "/"),
nvl(rs.getString("engine")), nvl(rs.getString("engine")),
@@ -46,6 +46,29 @@ public final class Database implements AutoCloseable {
st.executeUpdate(ddl); st.executeUpdate(ddl);
} }
} }
migrate();
}
/**
* Dokłada kolumny, których {@code CREATE TABLE IF NOT EXISTS} nie doda do już
* istniejącej tabeli. Tylko {@code ADD COLUMN} — bez przebudowy tabel, bo na
* {@code copy} wiszą klucze obce z {@code ON DELETE CASCADE}.
*/
private void migrate() throws SQLException {
addColumnIfMissing("copy", "fs_type", "TEXT");
}
private void addColumnIfMissing(String table, String column, String type) throws SQLException {
try (Statement st = connection.createStatement()) {
try (java.sql.ResultSet rs = st.executeQuery("PRAGMA table_info(" + table + ")")) {
while (rs.next()) {
if (column.equalsIgnoreCase(rs.getString("name"))) {
return;
}
}
}
st.executeUpdate("ALTER TABLE " + table + " ADD COLUMN " + column + " " + type);
}
} }
@Override @Override
@@ -98,6 +121,9 @@ public final class Database implements AutoCloseable {
// Kopia to nośnik, nie gra: jedna płyta bywa dwupackiem z dwoma drzewami // Kopia to nośnik, nie gra: jedna płyta bywa dwupackiem z dwoma drzewami
// gier, więc przypisanie do wydania wisi przy game_root, nie tutaj. // gier, więc przypisanie do wydania wisi przy game_root, nie tutaj.
// status mówi, czy zawartość w ogóle dało się odczytać. // status mówi, czy zawartość w ogóle dało się odczytać.
// container to rodzaj artefaktu (plik .iso / .zip / katalog na dysku),
// fs_type to system plików wykryty przez :core po zawartości — to nie to samo:
// Wojna Trojańska.iso jest plikiem .iso, ale w środku ma UDF, nie ISO9660.
""" """
CREATE TABLE IF NOT EXISTS copy ( CREATE TABLE IF NOT EXISTS copy (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@@ -105,6 +131,7 @@ public final class Database implements AutoCloseable {
status TEXT NOT NULL DEFAULT 'ok' status TEXT NOT NULL DEFAULT 'ok'
CHECK (status IN ('ok', 'installer', 'unreadable')), CHECK (status IN ('ok', 'installer', 'unreadable')),
status_note TEXT, status_note TEXT,
fs_type TEXT,
path TEXT NOT NULL UNIQUE, path TEXT NOT NULL UNIQUE,
display_name TEXT, display_name TEXT,
size INTEGER, size INTEGER,
@@ -1,6 +1,8 @@
package pl.genschu.rexcatalog.ingest; package pl.genschu.rexcatalog.ingest;
import pl.genschu.bloomooemulator.engine.filesystem.AssetSourceDispatcher; import pl.genschu.bloomooemulator.engine.filesystem.AssetSourceDispatcher;
import pl.genschu.bloomooemulator.engine.filesystem.FileSystemDetector;
import pl.genschu.bloomooemulator.engine.filesystem.FileSystemType;
import pl.genschu.bloomooemulator.engine.filesystem.IFileSystem; import pl.genschu.bloomooemulator.engine.filesystem.IFileSystem;
import pl.genschu.rexcatalog.db.Database; import pl.genschu.rexcatalog.db.Database;
@@ -25,7 +27,8 @@ import java.util.Map;
* Indeksuje pojedynczą kopię (ISO / ZIP / rozpakowany katalog) do bazy katalogu. * Indeksuje pojedynczą kopię (ISO / ZIP / rozpakowany katalog) do bazy katalogu.
* *
* <p>Nic nie rozpakowuje na dysk — czyta przez {@link IFileSystem} z {@code :core}, * <p>Nic nie rozpakowuje na dysk — czyta przez {@link IFileSystem} z {@code :core},
* czyli tę samą implementację ISO9660/Joliet, której używa emulator przy uruchamianiu gry. * czyli te same implementacje ISO9660/Joliet i UDF, których używa emulator
* przy uruchamianiu gry.
* Kopia, której nie da się odczytać, i tak trafia do bazy — ze statusem i powodem, * Kopia, której nie da się odczytać, i tak trafia do bazy — ze statusem i powodem,
* bo „mam ten obraz, ale go nie otwieram" to też fakt wart skatalogowania. * bo „mam ten obraz, ale go nie otwieram" to też fakt wart skatalogowania.
*/ */
@@ -54,14 +57,16 @@ public final class Ingestor {
Map<String, Long> sizeByPath = new LinkedHashMap<>(); Map<String, Long> sizeByPath = new LinkedHashMap<>();
Map<String, FormatSniffer.Result> formatByPath = new LinkedHashMap<>(); Map<String, FormatSniffer.Result> formatByPath = new LinkedHashMap<>();
long totalBytes; long totalBytes;
String fsType = null;
try { try {
// detekcja po zawartości, nie po rozszerzeniu — .iso bywa obrazem UDF
fsType = fsTypeName(FileSystemDetector.detectFileSystemType(source));
IFileSystem fs = AssetSourceDispatcher.openAssets(source); IFileSystem fs = AssetSourceDispatcher.openAssets(source);
totalBytes = walk(fs, "", sha1ByPath, sizeByPath, formatByPath); totalBytes = walk(fs, "", sha1ByPath, sizeByPath, formatByPath);
} catch (Exception e) { } catch (Exception e) {
// np. Wojna Trojańska.iso — obraz UDF, którego IsoFileSystem nie parsuje
String note = e.getMessage() == null ? e.toString() : e.getMessage(); String note = e.getMessage() == null ? e.toString() : e.getMessage();
persistUnreadable(source, container, containerSha256, note); persistUnreadable(source, container, fsType, containerSha256, note);
return new Result(source.getPath(), container, "unreadable", note, 0, 0, List.of()); return new Result(source.getPath(), container, "unreadable", note, 0, 0, List.of());
} }
@@ -74,7 +79,7 @@ public final class Ingestor {
} }
long containerSize = source.isDirectory() ? totalBytes : source.length(); long containerSize = source.isDirectory() ? totalBytes : source.length();
persist(source, container, status, statusNote, containerSize, containerSha256, persist(source, container, fsType, status, statusNote, containerSize, containerSha256,
sha1ByPath, sizeByPath, formatByPath, roots); sha1ByPath, sizeByPath, formatByPath, roots);
List<RootSummary> summaries = new ArrayList<>(); List<RootSummary> summaries = new ArrayList<>();
@@ -93,6 +98,16 @@ public final class Ingestor {
.orElse(null); .orElse(null);
} }
private static String fsTypeName(FileSystemType type) {
return switch (type) {
case DIRECTORY -> "dir";
case ZIP -> "zip";
case ISO9660 -> "iso9660";
case UDF -> "udf";
case UNKNOWN -> null;
};
}
private static String containerType(File source) throws IOException { private static String containerType(File source) throws IOException {
if (source.isDirectory()) { if (source.isDirectory()) {
return "dir"; return "dir";
@@ -154,13 +169,13 @@ public final class Ingestor {
return bytes; return bytes;
} }
private void persistUnreadable(File source, String container, String sha256, String note) private void persistUnreadable(File source, String container, String fsType,
throws Exception { String sha256, String note) throws Exception {
Connection conn = db.connection(); Connection conn = db.connection();
boolean auto = conn.getAutoCommit(); boolean auto = conn.getAutoCommit();
conn.setAutoCommit(false); conn.setAutoCommit(false);
try { try {
upsertCopy(conn, source, container, "unreadable", note, upsertCopy(conn, source, container, fsType, "unreadable", note,
source.length(), sha256, 0); source.length(), sha256, 0);
conn.commit(); conn.commit();
} catch (Exception e) { } catch (Exception e) {
@@ -171,7 +186,8 @@ public final class Ingestor {
} }
} }
private void persist(File source, String container, String status, String statusNote, private void persist(File source, String container, String fsType,
String status, String statusNote,
long size, String sha256, long size, String sha256,
Map<String, String> sha1ByPath, Map<String, String> sha1ByPath,
Map<String, Long> sizeByPath, Map<String, Long> sizeByPath,
@@ -181,7 +197,7 @@ public final class Ingestor {
boolean previousAutoCommit = conn.getAutoCommit(); boolean previousAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false); conn.setAutoCommit(false);
try { try {
long copyId = upsertCopy(conn, source, container, status, statusNote, long copyId = upsertCopy(conn, source, container, fsType, status, statusNote,
size, sha256, sha1ByPath.size()); size, sha256, sha1ByPath.size());
try (Statement st = conn.createStatement()) { try (Statement st = conn.createStatement()) {
@@ -261,30 +277,32 @@ public final class Ingestor {
} }
} }
private long upsertCopy(Connection conn, File source, String container, private long upsertCopy(Connection conn, File source, String container, String fsType,
String status, String statusNote, String status, String statusNote,
long size, String sha256, int fileCount) throws Exception { long size, String sha256, int fileCount) throws Exception {
String path = source.getAbsolutePath(); String path = source.getAbsolutePath();
try (PreparedStatement st = conn.prepareStatement(""" try (PreparedStatement st = conn.prepareStatement("""
INSERT INTO copy (container, status, status_note, path, display_name, INSERT INTO copy (container, fs_type, status, status_note, path, display_name,
size, sha256, file_count, ingested_at, core_version) size, sha256, file_count, ingested_at, core_version)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(path) DO UPDATE SET ON CONFLICT(path) DO UPDATE SET
container = excluded.container, status = excluded.status, container = excluded.container, fs_type = excluded.fs_type,
status = excluded.status,
status_note = excluded.status_note, size = excluded.size, status_note = excluded.status_note, size = excluded.size,
sha256 = excluded.sha256, file_count = excluded.file_count, sha256 = excluded.sha256, file_count = excluded.file_count,
ingested_at = excluded.ingested_at, core_version = excluded.core_version ingested_at = excluded.ingested_at, core_version = excluded.core_version
""")) { """)) {
st.setString(1, container); st.setString(1, container);
st.setString(2, status); st.setString(2, fsType);
st.setString(3, statusNote); st.setString(3, status);
st.setString(4, path); st.setString(4, statusNote);
st.setString(5, source.getName()); st.setString(5, path);
st.setLong(6, size); st.setString(6, source.getName());
st.setString(7, sha256); st.setLong(7, size);
st.setInt(8, fileCount); st.setString(8, sha256);
st.setString(9, Instant.now().toString()); st.setInt(9, fileCount);
st.setString(10, coreVersion); st.setString(10, Instant.now().toString());
st.setString(11, coreVersion);
st.executeUpdate(); st.executeUpdate();
} }
try (PreparedStatement st = conn.prepareStatement("SELECT id FROM copy WHERE path = ?")) { try (PreparedStatement st = conn.prepareStatement("SELECT id FROM copy WHERE path = ?")) {