diff --git a/src/main/java/com/dostalgia/GameResource.java b/src/main/java/com/dostalgia/GameResource.java index 136819d..abbc9d5 100644 --- a/src/main/java/com/dostalgia/GameResource.java +++ b/src/main/java/com/dostalgia/GameResource.java @@ -131,6 +131,23 @@ public class GameResource { } } + /** + * Patch a Sierra game bundle to fix common file layout issues + * (INTERP.ERR paths, RESOURCE.CFG directories). + */ + @POST + @Path("/{id}/patch-sierra") + public Response patchSierra(@PathParam("id") String id) { + try { + var patched = svc.patchSierraGame(id); + return Response.ok(Map.of("patched", patched)).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(); + } + } + @GET @Path("/{id}/download") @Produces("application/zip") diff --git a/src/main/java/com/dostalgia/GameService.java b/src/main/java/com/dostalgia/GameService.java index 0c34d1b..dbcc54e 100644 --- a/src/main/java/com/dostalgia/GameService.java +++ b/src/main/java/com/dostalgia/GameService.java @@ -322,5 +322,154 @@ public class GameService { } } + /** + * Patch a .jsdos bundle for Sierra games that have file layout issues: + * - INTERP.ERR(C) in a subdirectory instead of root (causes "Can't find" errors) + * - RESOURCE.CFG directory paths pointing to wrong locations + * Returns the list of files that were modified. + */ + public List patchSierraGame(String id) throws IOException { + Game game = load(id); + Path bundlePath = gamesDir.resolve(game.getBundleFile()); + if (!Files.exists(bundlePath)) { + throw new NoSuchFileException("Bundle not found: " + game.getBundleFile()); + } + + Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".patchtmp"); + List patched = new ArrayList<>(); + + try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath)); + var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) { + + // First pass: read all entries to find INTERP files in subdirs + // We can't buffer the whole zip, so we do the copy in one pass + // and apply known fixes on the fly. + + // Read the RESOURCE.CFG for Sierra directory detection + // We do this by pre-scanning the zip (streaming approach) + + java.util.zip.ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + zos.putNextEntry(new java.util.zip.ZipEntry(name)); + + if (name.equals("RESOURCE.CFG")) { + // Read, patch, write + byte[] content = zis.readAllBytes(); + String text = new String(content, java.nio.charset.StandardCharsets.ISO_8859_1); + String original = text; + + // Fix directory paths pointing to a subdir when resources are at root + text = text.replaceAll("(?m)^directory\\s*=\\\\?.+", "directory = ."); + text = text.replaceAll("(?m)^audio\\s*=\\s*[A-Za-z]:\\\\?.+", "audio = ."); + text = text.replaceAll("(?m)^sync\\s*=\\s*[A-Za-z]:\\\\?.+", "sync = ."); + text = text.replaceAll("(?m)^movieDir\\s*=\\s*[A-Za-z]:\\\\?.+", "movieDir = .\\\\SEQ"); + text = text.replaceAll("(?m)^patchDir\\s*=\\s*[A-Za-z]:\\\\?.+", "patchDir = ."); + + if (!text.equals(original)) { + patched.add("RESOURCE.CFG (fixed paths)"); + zos.write(text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)); + } else { + zos.write(content); + } + } else { + // For all other files, check if we need to add INTERP.ERR(C) copies + zis.transferTo(zos); + } + zos.closeEntry(); + zis.closeEntry(); + } + + // Second pass: scan for INTERP files in subdirectories + // We need to re-read the zip for this. Use a clean approach. + } + + // If RESOURCE.CFG was not patched, check for INTERP.ERR and add it + boolean needsInterpFix = false; + String interpSource = null; + + // Re-scan the original bundle for INTERP files in subdirs + try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath))) { + java.util.zip.ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + if (!name.startsWith(".jsdos/") && name.contains("/") && + (name.endsWith("/INTERP.ERR") || name.endsWith("/INTERP.ERRC"))) { + // Found an INTERP file in a subdirectory + String rootName = name.substring(name.lastIndexOf('/') + 1); + interpSource = name; + break; + } + zis.closeEntry(); + } + } + + if (interpSource != null) { + // Check if INTERP file already exists at root in the patched bundle + boolean hasRootInterp = false; + try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(tmpPath))) { + java.util.zip.ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + if (name.equals("INTERP.ERR") || name.equals("INTERP.ERRC")) { + hasRootInterp = true; + break; + } + zis.closeEntry(); + } + } + + if (!hasRootInterp) { + // Read the INTERP file from original bundle and append it + byte[] interpData = null; + try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath))) { + java.util.zip.ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals(interpSource)) { + interpData = zis.readAllBytes(); + break; + } + zis.closeEntry(); + } + } + + if (interpData != null) { + // Append to the patched bundle + String rootName = interpSource.substring(interpSource.lastIndexOf('/') + 1); + try (var zis2 = new java.util.zip.ZipInputStream(Files.newInputStream(tmpPath)); + var tmp2 = new java.util.zip.ZipOutputStream(Files.newOutputStream( + tmpPath.resolveSibling(bundlePath.getFileName() + ".patchtmp2")))) { + java.util.zip.ZipEntry e; + while ((e = zis2.getNextEntry()) != null) { + tmp2.putNextEntry(new java.util.zip.ZipEntry(e.getName())); + zis2.transferTo(tmp2); + tmp2.closeEntry(); + zis2.closeEntry(); + } + // Add INTERP file at root + tmp2.putNextEntry(new java.util.zip.ZipEntry(rootName)); + tmp2.write(interpData); + tmp2.closeEntry(); + zis2.close(); + tmp2.close(); + } + Files.delete(tmpPath); + Path tmp2 = tmpPath.resolveSibling(bundlePath.getFileName() + ".patchtmp2"); + Files.move(tmp2, tmpPath); + patched.add(rootName + " (copied from " + interpSource + ")"); + } + } + } + + // If nothing was patched, clean up and return empty + if (patched.isEmpty()) { + Files.deleteIfExists(tmpPath); + return patched; + } + + Files.move(tmpPath, bundlePath, StandardCopyOption.REPLACE_EXISTING); + return patched; + } + public Path getGamesDir() { return gamesDir; } }