feat: support CD-only games (no executable on filesystem)
Build & Deploy / build-and-deploy (push) Failing after 31s
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:
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user