feat: playable Win3.x and Win9.x using js-dos - #4061
Conversation
1c56c39 to
f827acc
Compare
|
Done. |
Amp-Thread-ID: https://ampcode.com/threads/T-019fcd57-445f-754d-9a5b-a45cf2878d99 Co-authored-by: Amp <amp@ampcode.com>
ea70764 to
dc11901
Compare
|
@gantoine, I think this is ready for an initial review when you have a chance. I tested saving plus if the launch/exit UX behave as expected. Please also take a look at the accompanying documentation PR. As mentioned, I used AI assistance for the initial design and subsequent implementation review. The integration went through several iterations to keep the scope focused and consistent with existing players. I am not an expert on this part of the codebase, so I would appreciate your feedback! Thank you |
There was a problem hiding this comment.
Pull request overview
Adds an in-browser js-dos (DOSBox-X) player path to RomM so Win3.x / Win9x ROMs can be played without the blank-screen issues seen with the existing EmulatorJS DOS core. This fits into the existing v2 player architecture alongside EmulatorJS and Ruffle, including cross-origin isolation support for SharedArrayBuffer and an admin-disable toggle surfaced via heartbeat.
Changes:
- Added a new v2 js-dos player view plus router wiring and play-action selection for
win3x/win9x. - Introduced
DISABLE_JSDOSacross backend config + heartbeat, and frontend support gating viauseCanPlay/ utils. - Bundled a pinned js-dos release into the full Docker image and extended Nginx COOP/COEP headers for the new player route.
Reviewed changes
Copilot reviewed 35 out of 37 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/v2/views/Player/JsDos.vue | New js-dos player view (runtime loader, lifecycle, local save scoping, quit flow). |
| frontend/src/v2/views/Player/JsDos.test.ts | Vitest coverage for js-dos player lifecycle, final-save flow, and save-key scoping. |
| frontend/src/v2/router/routes.ts | Registers the new v2 route component loader for jsdos. |
| frontend/src/v2/composables/useGameActions/index.ts | Routes Win3x/Win9x play actions to a hard navigation for /rom/:id/jsdos (SAB). |
| frontend/src/v2/composables/useGameActions/index.test.ts | Adds coverage ensuring js-dos is preferred over EmulatorJS for its platforms. |
| frontend/src/v2/composables/useCanPlay/index.ts | Adds canPlayJsDos and integrates it into the overall in-browser playability gate. |
| frontend/src/utils/index.ts | Adds isJsDosEmulationSupported() with DISABLE_JSDOS + platform slug support checks. |
| frontend/src/utils/index.test.ts | Unit tests for js-dos platform support logic, case-insensitivity, toggle behavior, and remap. |
| frontend/src/types/js-dos.d.ts | Adds TypeScript typings for the js-dos runtime factory and API surface used by the player. |
| frontend/src/stores/heartbeat.ts | Extends the default heartbeat shape with EMULATION.DISABLE_JSDOS. |
| frontend/src/plugins/router.ts | Adds the /rom/:rom/jsdos route and ROUTES.JSDOS constant. |
| frontend/src/locales/bg_BG/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/cs_CZ/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/de_DE/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/en_GB/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/en_US/play.json | Adds new play.* strings for js-dos save warning and quit confirmation (source locale). |
| frontend/src/locales/es_ES/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/fr_FR/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/hu_HU/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/it_IT/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/ja_JP/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/ko_KR/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/pl_PL/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/pt_BR/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/ro_RO/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/ru_RU/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/tr_TR/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/zh_CN/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/locales/zh_TW/play.json | Adds new play.* strings for js-dos save warning and quit confirmation. |
| frontend/src/generated/models/EmulationDict.ts | Updates generated OpenAPI types to include DISABLE_JSDOS. |
| env.template | Documents the new DISABLE_JSDOS env var. |
| docker/nginx/templates/default.conf.template | Adds COOP/COEP header mapping for the new /rom/*/jsdos player route. |
| docker/Dockerfile | Downloads and verifies js-dos release, then ships runtime assets into the full image. |
| backend/tests/endpoints/test_heartbeat.py | Extends heartbeat endpoint test to validate DISABLE_JSDOS is present and boolean. |
| backend/endpoints/responses/heartbeat.py | Extends the typed heartbeat response schema with DISABLE_JSDOS. |
| backend/endpoints/heartbeat.py | Includes DISABLE_JSDOS in the /heartbeat response payload. |
| backend/config/init.py | Adds env-var wiring for DISABLE_JSDOS. |
Files not reviewed (1)
- frontend/src/generated/models/EmulationDict.ts: Generated file
Suppressed comments (1)
frontend/src/v2/views/Player/JsDos.vue:186
- The "Back to gallery" navigation depends on
rom.value?.platform_id, which may be unset until the ROM request resolves. Use the seeded ROM data when available, and avoid pushing with an undefined param.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019fcd57-445f-754d-9a5b-a45cf2878d99
| // DOSBox-X provides Windows support. | ||
| dos = dosFactory(stage.value, { | ||
| url: getDownloadPath({ rom: currentRom }), | ||
| backend: "dosboxX", |
There was a problem hiding this comment.
There are multiple backends. I want to keep this PR minimal and adjust as needed rather than trying to replicate every possible 'knob'.
Resolve conflicts in useCanPlay and its useGameActions tests, where master added the has_file_on_disk gate to the play checks while this branch added the js-dos route. Keep both: js-dos also boots from the ROM file, so it gets the same gate as EmulatorJS and Ruffle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The js-dos release bundle ships a demo index.html that would be served unauthenticated under /assets/jsdos, plus source maps, Emscripten symbol tables and type declarations that no runtime code loads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
useCanPlay had no tests of its own, and useGameActions mocks it wholesale, so nothing exercised the has_file_on_disk gate that keeps a physical or missing-file game from offering a Play action. Add the missing suite, including a case per engine so the js-dos route cannot regain the gate. Also apply trunk fmt to a js-dos case in utils/index.test.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
usePlatformPlayable is the platform-level twin of useCanPlay, but only the latter learned about js-dos, so win3x/win9x games offered Play while their platform tile showed no playable badge and sorted into the unplayable bucket. Teach resolveEmulator about js-dos and derive `playable`/`isPlayable` from it instead of hand-maintained OR chains. Collapse the duplication the new backend introduced: a shared resolvePlatformSlug in utils, a supportedBy factory for useCanPlay's three identical computeds, one COOP/COEP nginx pattern per map, and a single hard-navigation branch in useGameActions that reuses the canPlay useCanPlay already returns. In JsDos.vue, share the teardown tail between leavePlayer and onBeforeUnmount, collapse four derivations of the route rom id into one, start the js-dos runtime alongside the rom request rather than behind it, and hold the rom in a shallowRef since the view only reads scalars off it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The js-dos runtime is only installed by docker/Dockerfile into the full image. Slim images ship without it, and the dev container has no emulator assets at all, so /assets/jsdos/js-dos.js resolved to the SPA fallback: 200 with index.html, which fails the stylesheet as text/html and rejects the script. EmulatorJS and Ruffle already fall back to a CDN in exactly this situation; js-dos was the only player without one, leaving it dead on slim images. Fall back to jsDelivr pinned to the same 8.4.1 the image bundles. It is the only source that is both version-pinned and sends `cross-origin-resource-policy: cross-origin`, so it still loads under the COOP and COEP headers nginx attaches to the player document; the official CDN sends no CORP header and has no versioned path. The emulator payloads follow whichever base served the runtime. Lift EmulatorJS's script injection and its content-type pre-flight into a shared module rather than adding a fourth copy. The pre-flight is what makes the fallback fire at all: a <script> tag "loads" an index.html body happily, so onerror never runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
js-dos applies the option at construction, and its browserSetFullScreen() calls exitFullscreen() unguarded on the false branch, which rejects with "Not in fullscreen mode" when the document was never fullscreen. Passing the option only when the preference is on keeps the launch free of an unhandled rejection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
js-dos boots exactly one format. Its `url` option runs the file through emulators.bundleConfig(), which requires an archive carrying `.jsdos/dosbox.conf`; handed an ordinary scanned game it panics with "Broken bundle, .jsdos/dosbox.conf not found" and the player never starts. Gate canPlayJsDos on the file extension so Play is offered only for files the player can actually boot, instead of routing a win3x/win9x ROM to a dead screen. The platform-level check is unchanged: usePlatformPlayable knows only a slug, and a win3x platform is still playable in browser given a bundle. Booting unbundled games would mean supplying `dosboxConf` and `initFs` ourselves, which needs entrypoint detection and, for Win9x, a system image. That is a feature in its own right, not a fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review-polish pass: cut the js-dos docstrings and the CDN-base comment back to the non-obvious fact, drop JSDOS_BUNDLE_EXTENSION now that it has one call site whose meaning is plain, and remove a redundant Partial<SimpleRom> cast from the new fixture helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds a v2 js-dos/DOSBox-X browser player for Windows 3.x and Windows 9x bundles, including per-user local saves and lifecycle handling.
Confidence Score: 4/5The query-sensitive isolation rule should be fixed before merging because valid js-dos URLs with query parameters cannot initialize the threaded emulator runtime. The new player depends on COOP and COEP, but Nginx matches the full request URI with a pattern that rejects any query suffix and consequently serves affected player documents without cross-origin isolation. Files Needing Attention: docker/nginx/templates/default.conf.template Important Files Changed
Prompt To Fix All With AI### Issue 1
docker/nginx/templates/default.conf.template:20-29
**Query strings disable isolation**
When a js-dos player URL contains a query string, matching against `$request_uri` fails the end-anchored pattern, so Nginx omits COOP and COEP and the threaded emulator runtime cannot initialize.
```suggestion
map $uri $coep_header {
default "";
~^/rom/.*/(ejs|jsdos)$ "require-corp";
~^/console/rom/[0-9]+/play "require-corp";
}
map $uri $coop_header {
default "";
~^/rom/.*/(ejs|jsdos)$ "same-origin";
~^/console/rom/[0-9]+/play "same-origin";
}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "refactor(jsdos): trim comments and inlin..." | Re-trigger Greptile |
…uery $request_uri includes the query string, so the end-anchored player pattern missed /rom/<id>/jsdos?x=1 and nginx dropped COOP and COEP, leaving the threaded runtime without SharedArrayBuffer. Accept an optional query instead of anchoring at the path end. Matching stays on $request_uri: try_files rewrites $uri to /index.html before add_header runs, so a $uri map would match no player URL at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thank you for looking into this! Read the commits you added and there were some gaps in the initial implementation, was good to learn! |
|
Just minor request to merge this one: rommapp/docs#110 Provides more insights on how to configure js-dos to co-exist with dosbox |
Closes #3143
The current EmulatorJS
dosbox-purecore handles MS-DOS games but blank-screens tested Windows 3.x and Windows 9x titles. This PR addsjs-dosv8.4.1 with its DOSBox-X backend as a dedicated in-browser player for thewin3xandwin9xplatforms.Related docs PR: rommapp/docs#110
Out of scope
To keep the integration narrow:
js-doscan also run DOS games.Changes
The integration follows the existing EmulatorJS and Ruffle patterns:
js-dosplayer and/rom/:rom/jsdosroute.DISABLE_JSDOSadministrator toggle.js-dosrelease under/assets/jsdosin the full Docker image.setNoCloud(true).Test plan
Automated tests cover player lifecycle, final-save handling, route departure, duplicate exits, and per-user save isolation. The remaining frontend, backend, build, and lint checks run in CI.
Tested titles:
Manual checks:
AI assistance disclosure
This contribution was developed with assistance from Amp and Claude. I reviewed, tested, and validated the design, implementation, and tests, and I take responsibility for the changes.