From 6f59028cb00c7e0303fb5386c71c6828d01ac3d1 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 3 Jun 2026 11:46:14 +0200 Subject: [PATCH] feat: support CD-only games (no executable on filesystem) 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 --- .../java/com/dostalgia/UploadResource.java | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index c6e12c2..b91bd34 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -83,9 +83,14 @@ public class UploadResource { // Find main executable String mainExe = findMainExe(extractDir); if (mainExe == null) { - return Response.status(400) - .entity(Map.of("error", "No executable (.exe/.com/.bat) found in the archive")) - .build(); + // No executable on the filesystem — check if CD images exist instead + List cdOnlyImages = findCdImages(extractDir); + 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.) @@ -101,7 +106,7 @@ public class UploadResource { createBundle(extractDir, mainExe, bundlePath); // 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. // 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.setPlatform(platform); // 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.setExecutables(findAllExecutables(extractDir)); if (setupExe != null) { @@ -438,11 +445,17 @@ public class UploadResource { // Write .jsdos config directly into the extract directory Path jsdos = extractDir.resolve(".jsdos"); Files.createDirectories(jsdos); - String relExe = extractDir.relativize(Path.of(exePath)).toString(); // Find CD images for DOSBox imgmount (mountable via D:, E:, …) List cdImages = findCdImages(extractDir); - Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages)); - Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe)); + if (exePath != null) { + 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) 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); } + /** Build config for a CD-only game (no executables on the filesystem). */ + private String buildCdOnlyDosboxConf(List 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 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 cdImages) { // DOS uses \\ as path separator; / is a switch character. // If the executable is in a subdirectory, cd into it first.