feat: include mount/imgmount in jsdos.json autoexec script + remove fix-paths endpoint
Build & Deploy / build-and-deploy (push) Successful in 36s
Build & Deploy / build-and-deploy (push) Successful in 36s
js-dos v8 uses the jsdos.json autoexec.script instead of the dosbox.conf [autoexec] section. Without mount/imgmount in the jsdos.json script, the CD image was never mounted — games loaded base data from C: (bundle root) but crashed when trying to access CD-ROM content (videos, audio). Also removes the now-unnecessary fix-paths endpoint (both endpoint and GameService implementation) since config path fixing is automatic during upload via the game-agnostic scanner.
This commit is contained in:
@@ -164,25 +164,6 @@ public class GameResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/{id}/fix-paths")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Response fixPaths(@PathParam("id") String id) {
|
|
||||||
try {
|
|
||||||
boolean patched = svc.fixBundleConfigPaths(id);
|
|
||||||
if (patched) {
|
|
||||||
return Response.ok(Map.of("status", "patched",
|
|
||||||
"message", "Fixed hardcoded DOS paths in game config")).build();
|
|
||||||
}
|
|
||||||
return Response.ok(Map.of("status", "noop",
|
|
||||||
"message", "No known config files needed patching")).build();
|
|
||||||
} catch (NoSuchFileException e) {
|
|
||||||
return Response.status(404).entity(Map.of("error", "Game not found")).build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Response.serverError().entity(Map.of("error", e.getMessage())).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Integer asInt(Object v) {
|
private Integer asInt(Object v) {
|
||||||
if (v instanceof Number n) return n.intValue();
|
if (v instanceof Number n) return n.intValue();
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public class GameService {
|
|||||||
// Build new config entries — preserve any CD images in the bundle
|
// Build new config entries — preserve any CD images in the bundle
|
||||||
List<String> cdImages = findCdImagesInBundle(bundlePath);
|
List<String> cdImages = findCdImagesInBundle(bundlePath);
|
||||||
byte[] newDosboxConf = buildDosboxConfBytes(executable, cdImages);
|
byte[] newDosboxConf = buildDosboxConfBytes(executable, cdImages);
|
||||||
byte[] newJsdosJson = buildJsdosJsonBytes(executable);
|
byte[] newJsdosJson = buildJsdosJsonBytes(executable, cdImages);
|
||||||
|
|
||||||
// Patch the ZIP: read old, write new with modified config entries
|
// Patch the ZIP: read old, write new with modified config entries
|
||||||
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
|
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
|
||||||
@@ -204,210 +204,6 @@ public class GameService {
|
|||||||
save(game);
|
save(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch hardcoded absolute DOS paths (C:\dir\ → .\) in game config files
|
|
||||||
* within a .jsdos bundle. Game-agnostic — scans all small text entries
|
|
||||||
* for drive-letter paths and rewrites stale directory prefixes.
|
|
||||||
*/
|
|
||||||
public boolean fixBundleConfigPaths(String id) throws IOException {
|
|
||||||
Game game = load(id);
|
|
||||||
String bundleFile = game.getBundleFile();
|
|
||||||
if (bundleFile == null) return false;
|
|
||||||
Path bundlePath = gamesDir.resolve(bundleFile);
|
|
||||||
if (!Files.exists(bundlePath)) return false;
|
|
||||||
|
|
||||||
// Phase 1: build a set of all entry paths for existence checking
|
|
||||||
java.util.Set<String> allEntries = new java.util.HashSet<>();
|
|
||||||
java.util.Set<String> dirs = new java.util.HashSet<>();
|
|
||||||
java.util.List<EntryData> textEntries = new java.util.ArrayList<>();
|
|
||||||
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath))) {
|
|
||||||
java.util.zip.ZipEntry entry;
|
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
|
||||||
String name = entry.getName();
|
|
||||||
allEntries.add(name);
|
|
||||||
// Add all parent directories too
|
|
||||||
int idx = name.lastIndexOf('/');
|
|
||||||
while (idx >= 0) {
|
|
||||||
dirs.add(name.substring(0, idx + 1));
|
|
||||||
idx = name.lastIndexOf('/', idx - 1);
|
|
||||||
}
|
|
||||||
if (!entry.isDirectory() && isSmallTextEntry(entry, zis)) {
|
|
||||||
byte[] data = zis.readAllBytes();
|
|
||||||
String text = new String(data, java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
if (hasDrivePath(text)) {
|
|
||||||
textEntries.add(new EntryData(name, data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zis.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textEntries.isEmpty()) return false;
|
|
||||||
|
|
||||||
// Build the existence checker using ZIP entry paths
|
|
||||||
var existsChecker = new ZipExistenceChecker(allEntries, dirs);
|
|
||||||
|
|
||||||
// Phase 2: patch matching entries and write new ZIP
|
|
||||||
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
|
|
||||||
boolean patched = false;
|
|
||||||
java.util.regex.Pattern drvPattern = java.util.regex.Pattern.compile(
|
|
||||||
"[A-Za-z]:(\\\\|/)[^\\s\"\\r\\n]+");
|
|
||||||
|
|
||||||
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
|
||||||
var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) {
|
|
||||||
|
|
||||||
java.util.zip.ZipEntry entry;
|
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
|
||||||
String name = entry.getName();
|
|
||||||
zos.putNextEntry(new java.util.zip.ZipEntry(name));
|
|
||||||
|
|
||||||
// Check if this entry needs patching
|
|
||||||
var ed = findEntryData(textEntries, name);
|
|
||||||
if (ed != null) {
|
|
||||||
String text = new String(ed.data, java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
String patchedText = patchZipPaths(text, existsChecker, drvPattern);
|
|
||||||
byte[] result = patchedText.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
if (!java.util.Arrays.equals(result, ed.data)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
zos.write(result);
|
|
||||||
} else {
|
|
||||||
zis.transferTo(zos);
|
|
||||||
}
|
|
||||||
zos.closeEntry();
|
|
||||||
zis.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patched) {
|
|
||||||
Files.move(tmpPath, bundlePath, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
} else {
|
|
||||||
Files.deleteIfExists(tmpPath);
|
|
||||||
}
|
|
||||||
return patched;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isSmallTextEntry(java.util.zip.ZipEntry entry, java.util.zip.ZipInputStream zis) {
|
|
||||||
if (entry.isDirectory()) return false;
|
|
||||||
long size = entry.getSize();
|
|
||||||
if (size > 100 * 1024 || size == 0) return true; // unknown size is OK
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasDrivePath(String text) {
|
|
||||||
return java.util.regex.Pattern.compile("[A-Za-z]:\\\\").matcher(text).find();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EntryData findEntryData(java.util.List<EntryData> list, String name) {
|
|
||||||
for (var ed : list) {
|
|
||||||
if (ed.name.equals(name)) return ed;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class EntryData {
|
|
||||||
final String name;
|
|
||||||
final byte[] data;
|
|
||||||
EntryData(String name, byte[] data) { this.name = name; this.data = data; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Case-insensitive existence check for paths inside a ZIP bundle. */
|
|
||||||
private static class ZipExistenceChecker {
|
|
||||||
final java.util.Set<String> entries; // all entries in original case
|
|
||||||
final java.util.Set<String> dirs; // all directory prefixes
|
|
||||||
|
|
||||||
ZipExistenceChecker(java.util.Set<String> entries, java.util.Set<String> dirs) {
|
|
||||||
this.entries = entries;
|
|
||||||
this.dirs = dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exists(String path) {
|
|
||||||
// Check both forward and backslash variants
|
|
||||||
String fwd = path.replace('\\', '/');
|
|
||||||
String bwd = path.replace('/', '\\');
|
|
||||||
// Check exact match
|
|
||||||
if (entries.contains(fwd) || entries.contains(bwd)) return true;
|
|
||||||
// Check case-insensitively
|
|
||||||
for (String e : entries) {
|
|
||||||
if (e.equalsIgnoreCase(fwd) || e.equalsIgnoreCase(bwd)) return true;
|
|
||||||
}
|
|
||||||
// Check as directory (trailing slash variants)
|
|
||||||
String fwdDir = fwd.endsWith("/") ? fwd : fwd + "/";
|
|
||||||
String bwdDir = bwd.endsWith("\\") ? bwd : bwd + "\\";
|
|
||||||
if (dirs.contains(fwdDir) || dirs.contains(bwdDir)) return true;
|
|
||||||
for (String d : dirs) {
|
|
||||||
if (d.equalsIgnoreCase(fwdDir) || d.equalsIgnoreCase(bwdDir)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Patch absolute DOS paths in ZIP entry text using the ZIP's own entry index. */
|
|
||||||
private static String patchZipPaths(String text, ZipExistenceChecker checker,
|
|
||||||
java.util.regex.Pattern drvPattern) {
|
|
||||||
var m = drvPattern.matcher(text);
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
int lastEnd = 0;
|
|
||||||
java.util.Set<String> seenPrefixes = new java.util.HashSet<>();
|
|
||||||
|
|
||||||
while (m.find()) {
|
|
||||||
sb.append(text, lastEnd, m.start());
|
|
||||||
String fullPath = m.group();
|
|
||||||
String drive = fullPath.substring(0, 2);
|
|
||||||
|
|
||||||
if (!"C:".equalsIgnoreCase(drive)) {
|
|
||||||
sb.append(fullPath);
|
|
||||||
lastEnd = m.end();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String sep = fullPath.substring(2, 3);
|
|
||||||
String relPath = fullPath.substring(3);
|
|
||||||
String separator = "\\".equals(sep) ? "\\\\" : "/";
|
|
||||||
String[] parts = relPath.split(separator, -1);
|
|
||||||
|
|
||||||
boolean found = false;
|
|
||||||
for (int skip = 0; skip < parts.length; skip++) {
|
|
||||||
StringBuilder suffix = new StringBuilder();
|
|
||||||
for (int j = skip; j < parts.length; j++) {
|
|
||||||
if (j > skip) suffix.append(sep);
|
|
||||||
suffix.append(parts[j]);
|
|
||||||
}
|
|
||||||
String suffixStr = suffix.toString();
|
|
||||||
if (suffixStr.isEmpty()) continue;
|
|
||||||
|
|
||||||
if (checker.exists(suffixStr)) {
|
|
||||||
if (skip == 0) {
|
|
||||||
sb.append(fullPath); // path valid
|
|
||||||
} else {
|
|
||||||
StringBuilder stale = new StringBuilder(drivePrefix(parts, skip, sep));
|
|
||||||
String staleStr = stale.toString();
|
|
||||||
if (!seenPrefixes.contains(staleStr)) {
|
|
||||||
seenPrefixes.add(staleStr);
|
|
||||||
}
|
|
||||||
sb.append(".").append(sep).append(suffixStr);
|
|
||||||
}
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) sb.append(fullPath);
|
|
||||||
lastEnd = m.end();
|
|
||||||
}
|
|
||||||
sb.append(text.substring(lastEnd));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String drivePrefix(String[] parts, int skip, String sep) {
|
|
||||||
StringBuilder sb = new StringBuilder("C:").append(sep);
|
|
||||||
for (int k = 0; k < skip; k++) {
|
|
||||||
if (k > 0) sb.append(sep);
|
|
||||||
sb.append(parts[k]);
|
|
||||||
}
|
|
||||||
sb.append(sep);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Scan a .jsdos bundle for CD image files (preserves the case from the ZIP entry). */
|
/** Scan a .jsdos bundle for CD image files (preserves the case from the ZIP entry). */
|
||||||
private List<String> findCdImagesInBundle(Path bundlePath) throws IOException {
|
private List<String> findCdImagesInBundle(Path bundlePath) throws IOException {
|
||||||
Set<String> cdExt = Set.of(".iso", ".cue", ".img", ".ccd", ".bin");
|
Set<String> cdExt = Set.of(".iso", ".cue", ".img", ".ccd", ".bin");
|
||||||
@@ -474,10 +270,23 @@ public class GameService {
|
|||||||
return sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
return sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build jsdos.json for a given executable path. */
|
/** Build jsdos.json for a given executable path and CD images. */
|
||||||
private byte[] buildJsdosJsonBytes(String relExe) {
|
private byte[] buildJsdosJsonBytes(String relExe, List<String> cdImages) {
|
||||||
var parts = splitExePath(relExe);
|
var parts = splitExePath(relExe);
|
||||||
StringBuilder script = new StringBuilder();
|
StringBuilder script = new StringBuilder();
|
||||||
|
// js-dos v8 uses the jsdos.json autoexec.script instead of the
|
||||||
|
// dosbox.conf [autoexec] section, so mount/imgmount MUST be here.
|
||||||
|
script.append("mount c .\n");
|
||||||
|
char driveLetter = 'D';
|
||||||
|
for (String img : cdImages) {
|
||||||
|
String imgLower = img.toLowerCase();
|
||||||
|
String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue"))
|
||||||
|
? " -t cdrom -fs iso" : " -t cdrom";
|
||||||
|
script.append("imgmount ").append(driveLetter).append(" \"")
|
||||||
|
.append(img).append("\"").append(flags).append("\n");
|
||||||
|
driveLetter++;
|
||||||
|
}
|
||||||
|
script.append("c:\n");
|
||||||
if (parts[0] != null) {
|
if (parts[0] != null) {
|
||||||
script.append("path=c:\\\n");
|
script.append("path=c:\\\n");
|
||||||
script.append("cd ").append(parts[0]).append("\n");
|
script.append("cd ").append(parts[0]).append("\n");
|
||||||
@@ -553,7 +362,7 @@ public class GameService {
|
|||||||
// Build new config entries — preserve any CD images in the bundle
|
// Build new config entries — preserve any CD images in the bundle
|
||||||
List<String> cdImages = findCdImagesInBundle(bundlePath);
|
List<String> cdImages = findCdImagesInBundle(bundlePath);
|
||||||
byte[] newDosboxConf = buildDosboxConfBytes(setupExe, cdImages);
|
byte[] newDosboxConf = buildDosboxConfBytes(setupExe, cdImages);
|
||||||
byte[] newJsdosJson = buildJsdosJsonBytes(setupExe);
|
byte[] newJsdosJson = buildJsdosJsonBytes(setupExe, cdImages);
|
||||||
|
|
||||||
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
||||||
var zos = new java.util.zip.ZipOutputStream(out)) {
|
var zos = new java.util.zip.ZipOutputStream(out)) {
|
||||||
|
|||||||
@@ -712,11 +712,11 @@ public class UploadResource {
|
|||||||
if (exePath != null) {
|
if (exePath != null) {
|
||||||
String relExe = extractDir.relativize(Path.of(exePath)).toString();
|
String relExe = extractDir.relativize(Path.of(exePath)).toString();
|
||||||
Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages));
|
Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages));
|
||||||
Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe));
|
Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe, cdImages));
|
||||||
} else {
|
} else {
|
||||||
// CD-only game — no executable, mount CD and show a DOS prompt
|
// CD-only game — no executable, mount CD and show a DOS prompt
|
||||||
Files.writeString(jsdos.resolve("dosbox.conf"), buildCdOnlyDosboxConf(cdImages));
|
Files.writeString(jsdos.resolve("dosbox.conf"), buildCdOnlyDosboxConf(cdImages));
|
||||||
Files.write(jsdos.resolve("jsdos.json"), buildCdOnlyJsdosJson());
|
Files.write(jsdos.resolve("jsdos.json"), buildCdOnlyJsdosJson(cdImages));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZIP it up directly from extractDir (no temp copy)
|
// ZIP it up directly from extractDir (no temp copy)
|
||||||
@@ -804,13 +804,25 @@ public class UploadResource {
|
|||||||
return new String[]{null, relExe};
|
return new String[]{null, relExe};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildJsdosJson(String relExe) {
|
private byte[] buildJsdosJson(String relExe, List<String> cdImages) {
|
||||||
// DOS uses \\ as path separator; / is a switch character.
|
// DOS uses \\ as path separator; / is a switch character.
|
||||||
// If the executable is in a subdirectory, cd into it first.
|
// js-dos v8 uses the jsdos.json autoexec.script instead of the
|
||||||
|
// dosbox.conf [autoexec] section, so mount/imgmount MUST be here.
|
||||||
var parts = splitDirExe(relExe);
|
var parts = splitDirExe(relExe);
|
||||||
StringBuilder script = new StringBuilder();
|
StringBuilder script = new StringBuilder();
|
||||||
|
script.append("mount c .\n");
|
||||||
|
// Mount CD images as D:, E:, … so games can find their CD.
|
||||||
|
char driveLetter = 'D';
|
||||||
|
for (String img : resolveCdImages(cdImages)) {
|
||||||
|
String imgLower = img.toLowerCase();
|
||||||
|
String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue"))
|
||||||
|
? " -t cdrom -fs iso" : " -t cdrom";
|
||||||
|
script.append("imgmount ").append(driveLetter).append(" \"")
|
||||||
|
.append(img).append("\"").append(flags).append("\n");
|
||||||
|
driveLetter++;
|
||||||
|
}
|
||||||
|
script.append("c:\n");
|
||||||
if (parts[0] != null) {
|
if (parts[0] != null) {
|
||||||
// Set PATH to include root so DOS extenders (DOS4GW etc.) are findable
|
|
||||||
script.append("path=c:\\\n");
|
script.append("path=c:\\\n");
|
||||||
script.append("cd ").append(parts[0]).append("\n");
|
script.append("cd ").append(parts[0]).append("\n");
|
||||||
}
|
}
|
||||||
@@ -892,10 +904,23 @@ public class UploadResource {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCdOnlyJsdosJson() {
|
private byte[] buildCdOnlyJsdosJson(List<String> cdImages) {
|
||||||
|
StringBuilder script = new StringBuilder();
|
||||||
|
script.append("mount c .\n");
|
||||||
|
char driveLetter = 'D';
|
||||||
|
for (String img : resolveCdImages(cdImages)) {
|
||||||
|
String imgLower = img.toLowerCase();
|
||||||
|
String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue"))
|
||||||
|
? " -t cdrom -fs iso" : " -t cdrom";
|
||||||
|
script.append("imgmount ").append(driveLetter).append(" \"")
|
||||||
|
.append(img).append("\"").append(flags).append("\n");
|
||||||
|
driveLetter++;
|
||||||
|
}
|
||||||
|
script.append("c:\n");
|
||||||
String json = "{"
|
String json = "{"
|
||||||
+ "\"autoexec\":{"
|
+ "\"autoexec\":{"
|
||||||
+ "\"options\":{\"script\":{\"value\":\"c:\\\\\"}}"
|
+ "\"options\":{\"script\":{\"value\":\""
|
||||||
|
+ escapeJson(script.toString()) + "\"}}"
|
||||||
+ "},"
|
+ "},"
|
||||||
+ "\"output\":{"
|
+ "\"output\":{"
|
||||||
+ "\"options\":{\"autolock\":{\"value\":true}}}"
|
+ "\"options\":{\"autolock\":{\"value\":true}}}"
|
||||||
|
|||||||
Reference in New Issue
Block a user