feat: support CD-only games (no executable on filesystem)
Build & Deploy / build-and-deploy (push) Failing after 31s

When no .exe/.com/.bat files are found but CD images are present,
the upload no longer fails. Instead it creates a CD-only game where
the autoexec mounts the CD-ROM image(s) and presents a DOS prompt
with instructions for the user.

- findMainExe returning null + CD images exist = CD-only game
- createBundle handles null exePath (generates CD-only config)
- New buildCdOnlyDosboxConf / buildCdOnlyJsdosJson methods
- Upload metadata handles null executable gracefully (empty string)
- Platform defaults to 'dos' when no executable to inspect
This commit is contained in:
Hermes Agent
2026-06-03 11:46:14 +02:00
parent 383c66b166
commit 6f59028cb0
@@ -83,9 +83,14 @@ public class UploadResource {
// Find main executable // Find main executable
String mainExe = findMainExe(extractDir); String mainExe = findMainExe(extractDir);
if (mainExe == null) { if (mainExe == null) {
return Response.status(400) // No executable on the filesystem — check if CD images exist instead
.entity(Map.of("error", "No executable (.exe/.com/.bat) found in the archive")) List<String> cdOnlyImages = findCdImages(extractDir);
.build(); if (cdOnlyImages.isEmpty()) {
return Response.status(400)
.entity(Map.of("error", "No executable (.exe/.com/.bat) found in the archive"))
.build();
}
// CD-only game (executable is on the CD image) — proceed without one
} }
// Find setup executable (SETUP.EXE, INSTALL.EXE, CONFIG.EXE, etc.) // Find setup executable (SETUP.EXE, INSTALL.EXE, CONFIG.EXE, etc.)
@@ -101,7 +106,7 @@ public class UploadResource {
createBundle(extractDir, mainExe, bundlePath); createBundle(extractDir, mainExe, bundlePath);
// Detect platform (DOS vs Windows) by reading the main EXE header // Detect platform (DOS vs Windows) by reading the main EXE header
String platform = detectPlatform(Path.of(mainExe)); String platform = mainExe != null ? detectPlatform(Path.of(mainExe)) : "dos";
// Store setup executable path if found and DOS-compatible. // Store setup executable path if found and DOS-compatible.
// Some games (e.g. Fallout) ship a Windows SETUP.EXE even though the main // Some games (e.g. Fallout) ship a Windows SETUP.EXE even though the main
@@ -113,7 +118,9 @@ public class UploadResource {
game.setBundleFile(bundleFile); game.setBundleFile(bundleFile);
game.setPlatform(platform); game.setPlatform(platform);
// Store the selected executable and the full list for the UI picker // Store the selected executable and the full list for the UI picker
String relMain = extractDir.relativize(Path.of(mainExe)).toString().replace('\\', '/'); String relMain = mainExe != null
? extractDir.relativize(Path.of(mainExe)).toString().replace('\\', '/')
: "";
game.setExecutable(relMain); game.setExecutable(relMain);
game.setExecutables(findAllExecutables(extractDir)); game.setExecutables(findAllExecutables(extractDir));
if (setupExe != null) { if (setupExe != null) {
@@ -438,11 +445,17 @@ public class UploadResource {
// Write .jsdos config directly into the extract directory // Write .jsdos config directly into the extract directory
Path jsdos = extractDir.resolve(".jsdos"); Path jsdos = extractDir.resolve(".jsdos");
Files.createDirectories(jsdos); Files.createDirectories(jsdos);
String relExe = extractDir.relativize(Path.of(exePath)).toString();
// Find CD images for DOSBox imgmount (mountable via D:, E:, …) // Find CD images for DOSBox imgmount (mountable via D:, E:, …)
List<String> cdImages = findCdImages(extractDir); List<String> cdImages = findCdImages(extractDir);
Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages)); if (exePath != null) {
Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe)); String relExe = extractDir.relativize(Path.of(exePath)).toString();
Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages));
Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe));
} else {
// CD-only game — no executable, mount CD and show a DOS prompt
Files.writeString(jsdos.resolve("dosbox.conf"), buildCdOnlyDosboxConf(cdImages));
Files.writeString(jsdos.resolve("jsdos.json"), buildCdOnlyJsdosJson());
}
// ZIP it up directly from extractDir (no temp copy) // ZIP it up directly from extractDir (no temp copy)
try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundlePath))) { try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundlePath))) {
@@ -550,6 +563,46 @@ public class UploadResource {
return json.getBytes(java.nio.charset.StandardCharsets.UTF_8); return json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
} }
/** Build config for a CD-only game (no executables on the filesystem). */
private String buildCdOnlyDosboxConf(List<String> cdImages) {
StringBuilder sb = new StringBuilder();
sb.append("[sdl]\nautolock=true\nusescancodes=true\noutput=surface\n\n");
sb.append("[cpu]\ncore=auto\ncycles=auto\n\n");
sb.append("[mixer]\nnosound=false\nrate=44100\n\n");
sb.append("[sblaster]\nsbtype=sb16\nirq=7\ndma=1\nhdma=5\n\n");
sb.append("[dos]\nxms=true\nems=true\numb=true\n\n");
sb.append("[autoexec]\n@echo off\nmount c .\n");
// Mount CD images as D:, E:, …
Set<String> seenDirs = new java.util.HashSet<>();
char driveLetter = 'D';
for (String img : cdImages) {
int slashIdx = img.lastIndexOf('/');
String parentDir = slashIdx >= 0 ? img.substring(0, slashIdx) : "";
if (!seenDirs.add(parentDir)) continue;
String imgLower = img.toLowerCase();
String flags = imgLower.endsWith(".bin") ? " -t cdrom -fs iso" : " -t cdrom";
sb.append("imgmount ").append(driveLetter).append(" \"").append(img).append("\"").append(flags).append("\n");
driveLetter++;
}
// Present a DOS prompt — the game runs from the CD
sb.append("c:\n");
sb.append("echo.\n");
sb.append("echo This game runs from the CD-ROM.\n");
sb.append("echo Type D: and press Enter, then look for the game executable (e.g. DOTT.EXE).\n");
return sb.toString();
}
private byte[] buildCdOnlyJsdosJson() {
String json = "{"
+ "\"autoexec\":{"
+ "\"options\":{\"script\":{\"value\":\"c:\\\\\"}}"
+ "},"
+ "\"output\":{"
+ "\"options\":{\"autolock\":{\"value\":true}}}"
+ "}";
return json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
private String buildDosboxConf(String relExe, List<String> cdImages) { private String buildDosboxConf(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. // If the executable is in a subdirectory, cd into it first.