fix: bypass js-dos bundleUpdateConfig by providing config + initFs separately
Build & Deploy / build-and-deploy (push) Successful in 1m16s
Build & Deploy / build-and-deploy (push) Successful in 1m16s
js-dos v8.3.8 calls bundleUpdateConfig which uses libzip WASM to
re-add .jsdos/jsdos.json into the downloaded bundle. This fails on
our bundles (Unable to add .jsdos/jsdos.json into bundle.zip).
Fix: Instead of using { url } option (which triggers bundleUpdateConfig),
fetch the config from a new API endpoint (/api/games/{id}/config) and
pass it directly via { dosboxConf, jsdosConf, initFs }. This bypasses
the broken bundleUpdateConfig entirely.
Changes:
- New GET /api/games/{id}/config endpoint (GameResource + GameService)
- Exposed resolveCdImages/pickBestCdImage as package-private
- Frontend Play.svelte: fetch config + bundle separately, use new API
- Frontend api.js: new getGameConfig() function
This commit is contained in:
@@ -111,6 +111,16 @@ export function bundleUrl(game) {
|
|||||||
return `/games/${game.bundle_file}`;
|
return `/games/${game.bundle_file}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get game config (dosbox.conf + jsdos.json) */
|
||||||
|
export async function getGameConfig(id) {
|
||||||
|
const res = await fetch(`${BASE}/api/games/${id}/config`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
/** Get game bundle URL for setup mode (runs SETUP.EXE instead) */
|
/** Get game bundle URL for setup mode (runs SETUP.EXE instead) */
|
||||||
export function setupBundleUrl(game) {
|
export function setupBundleUrl(game) {
|
||||||
if (!game) return null;
|
if (!game) return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getGame, bundleUrl, setupBundleUrl } from "../lib/api.js";
|
import { getGame, getGameConfig, bundleUrl, setupBundleUrl } from "../lib/api.js";
|
||||||
import { push } from "../lib/router.js";
|
import { push } from "../lib/router.js";
|
||||||
|
|
||||||
let { id } = $props();
|
let { id } = $props();
|
||||||
@@ -68,9 +68,27 @@
|
|||||||
|
|
||||||
// Start the DOS emulator
|
// Start the DOS emulator
|
||||||
try {
|
try {
|
||||||
const url = isSetup ? setupBundleUrl(game) : bundleUrl(game);
|
if (isSetup) {
|
||||||
|
// Setup mode: use the streaming setup-bundle endpoint
|
||||||
|
const url = setupBundleUrl(game);
|
||||||
if (!url || !window.Dos) throw new Error("Failed to resolve game bundle");
|
if (!url || !window.Dos) throw new Error("Failed to resolve game bundle");
|
||||||
dosCI = await window.Dos(dosContainer, { url });
|
dosCI = await window.Dos(dosContainer, { url });
|
||||||
|
} else {
|
||||||
|
// Normal mode: fetch config + bundle separately, use dosboxConf + initFs
|
||||||
|
// This bypasses js-dos's bundleUpdateConfig which fails on our bundles
|
||||||
|
const [config, bundleResp] = await Promise.all([
|
||||||
|
getGameConfig(game.id),
|
||||||
|
fetch(bundleUrl(game)),
|
||||||
|
]);
|
||||||
|
if (!bundleResp.ok) throw new Error("Failed to download game bundle");
|
||||||
|
const bundle = new Uint8Array(await bundleResp.arrayBuffer());
|
||||||
|
const jsdosConf = config.jsdos_conf ? JSON.parse(config.jsdos_conf) : undefined;
|
||||||
|
dosCI = await window.Dos(dosContainer, {
|
||||||
|
dosboxConf: config.dosbox_conf,
|
||||||
|
jsdosConf,
|
||||||
|
initFs: bundle,
|
||||||
|
});
|
||||||
|
}
|
||||||
running = true;
|
running = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
|
|||||||
@@ -153,6 +153,20 @@ public class GameResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}/config")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response config(@PathParam("id") String id) {
|
||||||
|
try {
|
||||||
|
var config = svc.getBundleConfig(id);
|
||||||
|
return Response.ok(config).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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
public Response delete(@PathParam("id") String id) {
|
public Response delete(@PathParam("id") String id) {
|
||||||
|
|||||||
@@ -371,5 +371,64 @@ public class GameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get bundle config (dosbox.conf + jsdos.json) for a game.
|
||||||
|
* Regenerates from stored metadata rather than reading from the bundle. */
|
||||||
|
public Map<String, Object> getBundleConfig(String id) throws IOException {
|
||||||
|
Game game = load(id);
|
||||||
|
String exe = game.getExecutable();
|
||||||
|
Path bundlePath = gamesDir.resolve(game.getBundleFile());
|
||||||
|
List<String> cdImages = exe != null && !exe.isBlank()
|
||||||
|
? findCdImagesInBundle(bundlePath)
|
||||||
|
: List.of();
|
||||||
|
|
||||||
|
byte[] dosboxConf;
|
||||||
|
byte[] jsdosConf;
|
||||||
|
if (exe != null && !exe.isBlank()) {
|
||||||
|
dosboxConf = buildDosboxConfBytes(exe, cdImages);
|
||||||
|
jsdosConf = buildJsdosJsonBytes(exe);
|
||||||
|
} else {
|
||||||
|
// CD-only game — generate CD-only config
|
||||||
|
dosboxConf = buildCdOnlyDosboxConfBytes(cdImages);
|
||||||
|
jsdosConf = buildCdOnlyJsdosJsonBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> config = new java.util.LinkedHashMap<>();
|
||||||
|
config.put("dosbox_conf", new String(dosboxConf, java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
config.put("jsdos_conf", new String(jsdosConf, java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build dosbox.conf for a CD-only game (no executable on filesystem). */
|
||||||
|
private byte[] buildCdOnlyDosboxConfBytes(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:, …
|
||||||
|
char driveLetter = 'D';
|
||||||
|
for (String img : UploadResource.resolveCdImages(cdImages)) {
|
||||||
|
String imgLower = img.toLowerCase();
|
||||||
|
String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue"))
|
||||||
|
? " -t cdrom -fs iso" : " -t cdrom";
|
||||||
|
sb.append("imgmount ").append(driveLetter).append(" \"").append(img).append("\"").append(flags).append("\n");
|
||||||
|
driveLetter++;
|
||||||
|
}
|
||||||
|
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().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build jsdos.json for a CD-only game. */
|
||||||
|
private byte[] buildCdOnlyJsdosJsonBytes() {
|
||||||
|
String json = "{\"autoexec\":{\"options\":{\"script\":{\"value\":\"c:\\\\\"}}},"
|
||||||
|
+ "\"output\":{\"options\":{\"autolock\":{\"value\":true}}}}";
|
||||||
|
return json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
public Path getGamesDir() { return gamesDir; }
|
public Path getGamesDir() { return gamesDir; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ public class UploadResource {
|
|||||||
/** Resolve one CD image per parent directory with best-format priority.
|
/** Resolve one CD image per parent directory with best-format priority.
|
||||||
* Priority: .cue > .iso > .ccd > .img > .bin
|
* Priority: .cue > .iso > .ccd > .img > .bin
|
||||||
* js-dos DOSBox (Emscripten) supports .cue natively, but .ccd often fails. */
|
* js-dos DOSBox (Emscripten) supports .cue natively, but .ccd often fails. */
|
||||||
private static List<String> resolveCdImages(List<String> cdImages) {
|
static List<String> resolveCdImages(List<String> cdImages) {
|
||||||
// Group by parent directory
|
// Group by parent directory
|
||||||
java.util.Map<String, java.util.List<String>> byDir = new java.util.LinkedHashMap<>();
|
java.util.Map<String, java.util.List<String>> byDir = new java.util.LinkedHashMap<>();
|
||||||
for (String img : cdImages) {
|
for (String img : cdImages) {
|
||||||
@@ -651,7 +651,7 @@ public class UploadResource {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String pickBestCdImage(java.util.List<String> images) {
|
static String pickBestCdImage(java.util.List<String> images) {
|
||||||
// Priority: .cue [5] > .iso [4] > .ccd [3] > .img [2] > .bin [1]
|
// Priority: .cue [5] > .iso [4] > .ccd [3] > .img [2] > .bin [1]
|
||||||
String best = null;
|
String best = null;
|
||||||
int bestScore = -1;
|
int bestScore = -1;
|
||||||
|
|||||||
Reference in New Issue
Block a user