feat: SETUP.EXE detection + separate setup .jsdos bundle
Build & Deploy / build-and-deploy (push) Successful in 1m1s
Build & Deploy / build-and-deploy (push) Successful in 1m1s
When a game archive contains SETUP.EXE, INSTALL.EXE, CONFIG.EXE or
CUSTOM.EXE (or .com/.bat variants), the upload now:
- Detects and stores the path in game metadata (setup_exe / has_setup)
- Generates a second .jsdos bundle with SETUP.EXE as autoexec:
- Standard mode: {gameId}.setup.jsdos (full game + setup autoexec)
- Sockdrive mode: .jsdos-setup/ directory alongside .jsdos/
- Both bundles share the same game files; only the autoexec differs
Frontend changes:
- Router strips query params from hash so ?setup=1 doesn't leak into id
- GameDetail shows a "Setup" button between Play and Edit (only when
game.has_setup is true), navigating to /play/{id}?setup=1
- Play.svelte detects ?setup=1, loads setupBundleUrl() instead, and
shows setup-specific overlay text
Persistence is handled automatically by js-dos v8 via the built-in
auto-save mechanism (ci.persist() → browser OPFS), so any config
changes made via SETUP.EXE survive across sessions.
This commit is contained in:
@@ -22,6 +22,8 @@ public class Game {
|
||||
private String bundleType; // "standard" or "sockdrive"
|
||||
private String bundleFile; // relative path within game dir
|
||||
|
||||
private String setupExe; // relative path to SETUP.EXE (null if none)
|
||||
|
||||
private Integer igdbId;
|
||||
private String igdbCoverId;
|
||||
|
||||
@@ -81,6 +83,12 @@ public class Game {
|
||||
public String getBundleFile() { return bundleFile; }
|
||||
public void setBundleFile(String bundleFile) { this.bundleFile = bundleFile; }
|
||||
|
||||
public String getSetupExe() { return setupExe; }
|
||||
public void setSetupExe(String setupExe) { this.setupExe = setupExe; }
|
||||
|
||||
/** Returns true if a SETUP.EXE (or variant) was detected in the game archive. */
|
||||
public boolean isHasSetup() { return setupExe != null && !setupExe.isBlank(); }
|
||||
|
||||
public Integer getIgdbId() { return igdbId; }
|
||||
public void setIgdbId(Integer igdbId) { this.igdbId = igdbId; }
|
||||
|
||||
|
||||
@@ -121,6 +121,24 @@ public class GameService {
|
||||
}
|
||||
// Remove flat .jsdos bundle
|
||||
Files.deleteIfExists(gamesDir.resolve(id + ".jsdos"));
|
||||
// Remove flat setup .jsdos bundle (if any)
|
||||
Files.deleteIfExists(gamesDir.resolve(id + ".setup.jsdos"));
|
||||
// Remove .jsdos-setup dir (sockdrive mode, if any)
|
||||
Path setupDir = dir.resolve(".jsdos-setup");
|
||||
if (Files.exists(setupDir)) {
|
||||
Files.walkFileTree(setupDir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path f, BasicFileAttributes a) throws IOException {
|
||||
Files.delete(f);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path d, IOException e) throws IOException {
|
||||
Files.delete(d);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ public class UploadResource {
|
||||
.build();
|
||||
}
|
||||
|
||||
// Find setup executable (SETUP.EXE, INSTALL.EXE, CONFIG.EXE, etc.)
|
||||
String setupExe = findSetupExe(extractDir);
|
||||
|
||||
// Determine bundle strategy
|
||||
double sizeMb = dirSizeMB(extractDir);
|
||||
String bundleType = sizeMb > SOCKDRIVE_THRESHOLD_MB ? "sockdrive" : "standard";
|
||||
@@ -92,17 +95,33 @@ public class UploadResource {
|
||||
Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe));
|
||||
Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe));
|
||||
bundleFile = ".jsdos/dosbox.conf";
|
||||
|
||||
// Write setup config if a setup executable was found
|
||||
if (setupExe != null) {
|
||||
Path setupDir = gameDir.resolve(".jsdos-setup");
|
||||
Files.createDirectories(setupDir);
|
||||
String relSetup = extractDir.relativize(Path.of(setupExe)).toString();
|
||||
Files.writeString(setupDir.resolve("dosbox.conf"), buildSetupDosboxConf(relSetup));
|
||||
Files.write(setupDir.resolve("jsdos.json"), buildSetupJsdosJson(relSetup));
|
||||
}
|
||||
} else {
|
||||
// Create .jsdos bundle (flat in games dir)
|
||||
bundleFile = gameId + ".jsdos";
|
||||
Path bundlePath = svc.getGamesDir().resolve(bundleFile);
|
||||
createBundle(extractDir, mainExe, bundlePath);
|
||||
|
||||
// Create setup bundle if a setup executable was found
|
||||
if (setupExe != null) {
|
||||
Path setupBundlePath = svc.getGamesDir().resolve(gameId + ".setup.jsdos");
|
||||
createBundle(extractDir, setupExe, setupBundlePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Save metadata
|
||||
Game game = new Game(gameId, title);
|
||||
game.setBundleType(bundleType);
|
||||
game.setBundleFile(bundleFile);
|
||||
game.setSetupExe(setupExe != null ? extractDir.relativize(Path.of(setupExe)).toString() : null);
|
||||
game.setReady(true);
|
||||
svc.save(game);
|
||||
|
||||
@@ -236,6 +255,46 @@ public class UploadResource {
|
||||
return candidates.getFirst().path();
|
||||
}
|
||||
|
||||
// ─── Setup Executable Detection ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Find a setup/install/config executable in the extracted game directory.
|
||||
* Looks for files named setup.exe/com/bat, install.exe/com/bat,
|
||||
* config.exe/com, customize.exe/com etc.
|
||||
*/
|
||||
private String findSetupExe(Path dir) throws IOException {
|
||||
record Candidate(int depth, long size, String path) {}
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
|
||||
List<String> patterns = List.of("setup", "install", "config", "custom");
|
||||
|
||||
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
|
||||
String name = f.getFileName().toString().toLowerCase();
|
||||
String stem = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : name;
|
||||
if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat"))
|
||||
return FileVisitResult.CONTINUE;
|
||||
for (String pat : patterns) {
|
||||
if (stem.equals(pat) || stem.startsWith(pat + ".") || stem.startsWith(pat + "-")) {
|
||||
int depth = f.getNameCount() - dir.getNameCount();
|
||||
candidates.add(new Candidate(depth, a.size(), f.toString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
if (candidates.isEmpty()) return null;
|
||||
// Prefer shallow (root-level) files, then smaller files (setup utilities tend to be small)
|
||||
candidates.sort(Comparator
|
||||
.comparingInt(Candidate::depth)
|
||||
.thenComparingLong(Candidate::size));
|
||||
return candidates.getFirst().path();
|
||||
}
|
||||
|
||||
|
||||
// ─── Bundle Creation ─────────────────────────────────────────
|
||||
|
||||
private void createBundle(Path extractDir, String mainExe, Path bundlePath) throws IOException {
|
||||
@@ -321,6 +380,57 @@ public class UploadResource {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
// ─── Setup Config Builders ───────────────────────────────────
|
||||
|
||||
private byte[] buildSetupJsdosJson(String relSetup) {
|
||||
String json = "{"
|
||||
+ "\"autoexec\":{"
|
||||
+ "\"options\":{\"script\":{\"value\":\"" + escapeJson(relSetup) + "\"}}"
|
||||
+ "},"
|
||||
+ "\"output\":{"
|
||||
+ "\"options\":{\"autolock\":{\"value\":true}}}"
|
||||
+ "}";
|
||||
return json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String buildSetupDosboxConf(String relSetup) {
|
||||
return "[sdl]\n"
|
||||
+ "autolock=true\n"
|
||||
+ "usescancodes=true\n"
|
||||
+ "output=surface\n"
|
||||
+ "\n"
|
||||
+ "[cpu]\n"
|
||||
+ "core=auto\n"
|
||||
+ "cycles=auto\n"
|
||||
+ "\n"
|
||||
+ "[mixer]\n"
|
||||
+ "nosound=false\n"
|
||||
+ "rate=44100\n"
|
||||
+ "\n"
|
||||
+ "[sblaster]\n"
|
||||
+ "sbtype=sb16\n"
|
||||
+ "irq=7\n"
|
||||
+ "dma=1\n"
|
||||
+ "hdma=5\n"
|
||||
+ "\n"
|
||||
+ "[dos]\n"
|
||||
+ "xms=true\n"
|
||||
+ "ems=true\n"
|
||||
+ "umb=true\n"
|
||||
+ "\n"
|
||||
+ "[autoexec]\n"
|
||||
+ "@echo off\n"
|
||||
+ "mount c .\n"
|
||||
+ "c:\n"
|
||||
+ "cls\n"
|
||||
+ "echo DOS Setup Utility\n"
|
||||
+ "echo -----------------\n"
|
||||
+ "echo Configure controls, sound, and other settings.\n"
|
||||
+ "echo When done, exit to return here. Changes are saved automatically.\n"
|
||||
+ "echo.\n"
|
||||
+ relSetup + "\n";
|
||||
}
|
||||
|
||||
// ─── File Utilities ──────────────────────────────────────────
|
||||
|
||||
private double dirSizeMB(Path dir) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user