Skip to content

feat: playable Win3.x and Win9.x using js-dos - #4061

Merged
gantoine merged 12 commits into
rommapp:masterfrom
bphenriques:3143-jsdos-backend
Aug 29, 2026
Merged

feat: playable Win3.x and Win9.x using js-dos#4061
gantoine merged 12 commits into
rommapp:masterfrom
bphenriques:3143-jsdos-backend

Conversation

@bphenriques

@bphenriques bphenriques commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Closes #3143

The current EmulatorJS dosbox-pure core handles MS-DOS games but blank-screens tested Windows 3.x and Windows 9x titles. This PR adds js-dos v8.4.1 with its DOSBox-X backend as a dedicated in-browser player for the win3x and win9x platforms.

Related docs PR: rommapp/docs#110

Out of scope

To keep the integration narrow:

  • MS-DOS remains on EmulatorJS, even though js-dos can also run DOS games.
  • Saves remain browser-local and are not synchronized with RomM.
  • Runtime readiness and bundle validation are deferred.

Changes

The integration follows the existing EmulatorJS and Ruffle patterns:

  • Adds the v2 js-dos player and /rom/:rom/jsdos route.
  • Adds the DISABLE_JSDOS administrator toggle.
  • Bundles a pinned, checksummed js-dos release under /assets/jsdos in the full Docker image.
  • Serves runtime assets same-origin with the required cross-origin isolation headers.
  • Enables DOSBox-X, automatic startup, and automatic saving.
  • Stores saves locally, isolated by RomM user and ROM.
  • Performs a final save before leaving and handles unconfirmed save failures.
  • Hides the dos.zone cloud UI with setNoCloud(true).
  • Adds API types and automated coverage for routing, persistence, and player lifecycle.

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:

  • Fine Artist, which saves only after exiting the application back to Windows
  • Four Magic School Bus titles
  • Timon & Pumbaa's Jungle Games

Manual checks:

  • Games start automatically.
  • Saves persist between sessions.
  • Saves are isolated between RomM users.
  • Quitting returns to the game details page.
  • The dos.zone cloud UI is hidden.

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.

@gantoine
gantoine self-requested a review August 1, 2026 23:10
@gantoine gantoine added the on-hold Pending further research or blocked by another issue label Aug 1, 2026
@bphenriques
bphenriques force-pushed the 3143-jsdos-backend branch 2 times, most recently from 1c56c39 to f827acc Compare August 3, 2026 10:45
@bphenriques

bphenriques commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Actions to review:

Will review later this week once my weekly limits resets. Taking a look by hand in the meantime (just slower).

Done.

@bphenriques bphenriques changed the title [WIP] feat: playable Win3.x and Win9.x using js-dos feat: playable Win3.x and Win9.x using js-dos Aug 16, 2026
@bphenriques

Copy link
Copy Markdown
Contributor Author

@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

@bphenriques
bphenriques marked this pull request as ready for review August 16, 2026 18:17
Copilot AI lite review requested due to automatic review settings August 16, 2026 18:17
Comment thread frontend/src/locales/bg_BG/play.json
Comment thread frontend/src/locales/pt_BR/play.json
Comment thread frontend/src/types/js-dos.d.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_JSDOS across backend config + heartbeat, and frontend support gating via useCanPlay / 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.

Comment thread frontend/src/v2/views/Player/JsDos.vue Outdated
Comment thread frontend/src/v2/views/Player/JsDos.vue
Comment thread frontend/src/v2/views/Player/JsDos.vue Outdated
// DOSBox-X provides Windows support.
dos = dosFactory(stage.value, {
url: getDownloadPath({ rom: currentRom }),
backend: "dosboxX",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple backends. I want to keep this PR minimal and adjust as needed rather than trying to replicate every possible 'knob'.

@gantoine gantoine removed the on-hold Pending further research or blocked by another issue label Aug 29, 2026
gantoine and others added 4 commits August 29, 2026 09:33
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>
gantoine and others added 3 commits August 29, 2026 11:21
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>
@gantoine

Copy link
Copy Markdown
Member

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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.

  • Adds js-dos platform and bundle eligibility, player routing, runtime loading, and play-session integration.
  • Adds the administrator disable flag through backend heartbeat and frontend capability state.
  • Packages pinned js-dos assets in the full image and configures cross-origin-isolation headers.
  • Adds player, routing, persistence, and lifecycle tests.

Confidence Score: 4/5

The 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

Filename Overview
frontend/src/v2/views/Player/JsDos.vue Implements runtime loading, DOSBox-X startup, user-and-ROM-scoped browser saves, final-save confirmation, and player teardown.
frontend/src/v2/composables/useGameActions/index.ts Routes eligible Windows bundles to js-dos while preserving streaming priority and hard navigation for isolation headers.
frontend/src/v2/composables/useCanPlay/index.ts Adds file-sensitive js-dos playability alongside EmulatorJS and Ruffle.
docker/Dockerfile Downloads, verifies, trims, and installs js-dos 8.4.1 assets into full images.
docker/nginx/templates/default.conf.template Extends cross-origin isolation to js-dos, but query-bearing player URLs fail the end-anchored $request_uri match.
frontend/src/plugins/router.ts Registers the shared js-dos URL and its v2 named view.
backend/endpoints/heartbeat.py Exposes the new js-dos administrator toggle through the heartbeat capability response.

Fix all with Greploop Fix All in Claude Code

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

Comment thread docker/nginx/templates/default.conf.template
…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>
@gantoine
gantoine merged commit 55065ef into rommapp:master Aug 29, 2026
10 checks passed
@bphenriques

Copy link
Copy Markdown
Contributor Author

Thank you for looking into this! Read the commits you added and there were some gaps in the initial implementation, was good to learn!

@bphenriques

Copy link
Copy Markdown
Contributor Author

Just minor request to merge this one: rommapp/docs#110

Provides more insights on how to configure js-dos to co-exist with dosbox

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Introduce JS-DOS as a new DOS backend

3 participants