Browser filesystem: Patch 9 (experimental)
The game-data importer writes immutable installation generations to the
origin-private file system at openrealm/game-data/wc3/install-*, with the
active manifest in openrealm/game-data/wc3/installation.json.
The launcher checks manifest versions 1, 2 and 3, file names/sizes, byte totals,
required War3.mpq, and MPQ header/table extents before native startup.
Manifest version 1 mounts /opfs/openrealm/game-data/wc3; versions 2 and 3 mount
/opfs/openrealm/game-data/wc3/<generation> via the engine's -data argument.
Version 3 permits strictly validated nested Maps/... paths (not arbitrary
relative paths); versions 1 and 2 continue to accept their existing flat
archive files. These checks are only format checks, not integrity hashes or
complete game-data validation. Importing a new generation does not alter already-open files.
The MPQ structural check validates declared lengths, supported header versions,
block/hash table offsets and counts, but cannot prove the payloads are playable.
The importer worker can read multiple ISO 9660/Joliet images and mix them with
direct MPQ/W3M/W3X uploads in one import. It selects recognized WC3 archives
from each disc and stores their source byte ranges in a version-3
generation. The combined selection must contain War3.mpq; duplicate archive
names are errors, not implicit overrides. Patch 5 additionally extracts a
known original RoC Setup.mpq map profile into nested Maps/ directories.
Patch 6 can recover directly named War3x.mpq/War3xLocal.mpq members from
a TFT Setup.mpq. The follow-on demo import patch accepts the documented
September 2002 demo disc separately and identifies it as demo in the manifest.
Patch 7 additionally
scans uncompressed anonymous installer blocks for nested MPQs, assigning game
archive filenames only where internal Warcraft III file-path hash evidence
identifies their roles. It also recognizes named base/local/patch members. It refuses to commit
a selected TFT installer that cannot supply War3x.mpq, rather than silently
claiming an expansion installation.
Setup.mpq itself is not a playable archive and is never installed.
Both browser pages expose ISO selection from patch 4. See
browser ISO reader.
Before staging, the worker asks navigator.storage.estimate() for remaining
quota when available. An explicit shortage prevents an import; absence or
failure of this optional estimate does not prevent trying to write. OPFS can
still fail during a write, in which case the uncommitted generation is removed
and the active manifest is retained. Importing again replaces the manifest
rather than merging with the current installation; select both RoC and TFT in
the same operation. The importer and launcher coordinate through the origin-scoped Web Locks API:
- The importer holds openrealm-wc3-installation exclusively for imports or removals.
- The launcher holds the same lock in shared mode while resolving the manifest
and acquiring a shared lease named openrealm-wc3-generation:<generation>
(or :legacy for version-1 root installations).
- That lease lasts for the running page's lifetime; generation garbage collection
obtains a non-blocking exclusive lease before deleting an old generation.
- After successful commit the current and immediately previous generation remain;
earlier unleased generations are reclaimed. Stale generations are also
pruned ahead of the quota estimate so they do not block future imports.
Busy generations are skipped and can be collected after closing their game
tabs and importing again. Removal
fails without deleting data if any game generation is leased. This keeps an
extra preceding generation for older tabs opened before Web Locks were added.
Web Locks are required for safe installation and engine startup; the UI reports unsupported browsers instead of silently allowing unsafe concurrent commits. Cross-tab serialization is origin-specific; a different browser profile/origin has independent storage. Interrupted uncommitted generations are still cleaned up by the worker; incomplete legacy leftovers can be reclaimed on a later successful import. Browser storage estimates are advisory, and persistent-storage permission is not requested by the worker.
The browser engine uses Emscripten WasmFS (-sWASMFS=1) and mounts the upstream
OPFS backend at /opfs before Sys_ResolveShareDirectory, Com_Init, or
archive discovery. The existing common/mpq.c uses normal seek/read operations;
archives are not copied into MEMFS. There is no 256 MiB per-file or 512 MiB
total staging limit. Browser storage quotas and address-space limits still
apply, and compatibility with large archives is not yet established.
The WasmFS OPFS backend must not initialize on the browser main thread.
The experimental engine build enables pthreads and PROXY_TO_PTHREAD. Hosting
must provide a secure context and COOP/COEP cross-origin isolation to allow
SharedArrayBuffer. Confirm worker startup before testing actual game assets.
Save compatibility: The legacy Patch 5 IDBFS directory is not deleted or
migrated by Patch 9. WasmFS does not automatically mount that legacy directory.
Existing saves remain in browser storage but may be inaccessible until a
future explicit migration. Do not delete site data or assume a completed save
migration. Browser user-directory defaults also require validation before
claiming save persistence is complete.
The native engine build remains experimental and has not been compiled or run as part of this patch. A runtime probe must verify mounting and seekable reads, including seek/EOF, >256 MiB archives, and multi-tab behavior. On OPFS failure the browser build should fail startup rather than stage full archives.
To test locally, serve build output over HTTPS (or localhost) with headers:
Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp. The requirements apply to the
worker-backed game, not just the launcher. See docs/platform/browser.md.
Patch 4 adds worker event-driven browser UI: phase updates and a validated
per-source archive list precede byte-copy progress, while a final done
confirms that the manifest has been committed. The foundation build now
copies the importer scripts next to the emitted HTML; the engine build already
did so. Browser UI behavior is exercised by
node --test port/web/tests/installer-ui.test.cjs; see
browser ISO reader for the message contract.
ISO importer hardening
port/web/site/mpq-header.js is shared between install-worker.js and
filesystem-bridge.js: it checks the MPQ signature within the first 1024
bytes, version 0/1, header size, declared archive bounds, sector shift,
positive power-of-two hash count, nonzero block count and both table extents.
Standalone .mpq inputs may have up to 4096 bytes of trailing
signature/padding data beyond the declared archive; map files may
have an application-specific prefix. It does not decrypt every file,
validate CRCs, or guarantee playable game data.
A complete base import requires War3.mpq. An import that includes
War3x.mpq but lacks War3xLocal.mpq is marked
completeness: "partial-expansion" and lists missing: ["War3xLocal.mpq"]
in the version-3 manifest. It is retained for users with incomplete direct
uploads but is visibly labelled partial, not full Frozen Throne support.
War3xLocal.mpq without War3x.mpq is rejected. A selected TFT installer
that yields no War3x.mpq still fails without replacing the prior manifest.
Neither field proves the game is complete; additional patch/archive versions,
localization and proprietary game-data coverage need real-disc tests.
The importer now recognizes the documented demo disc by the root files
War3Demo.ico and SCDemo.exe, independent of ISO filename. Unlike prior
rejection behavior, demo imports are allowed and recorded as
edition: "demo", completeness: "demo" in the existing version-3 manifest.
War3.mpq still passes normal structure validation. Directly available
RetailMaps/*.w3m and .w3x files are copied to Maps/<filename> for the
normal map search path. Demo Setup.mpq is not interpreted with the retail
RoC installer-map profile. A demo disc cannot be mixed with retail ISOs or
standalone MPQs, though standalone map files may accompany it.
installer.js displays a persistent demo notice after importing and on
later reloads (list), separate from the transient progress message. The
engine launcher also identifies the demo on startup. Old manifests without
an edition field remain valid and are treated as retail/unclassified. The
launcher rejects unknown edition values or contradictory demo completeness
metadata, but edition is informational, not a verification of game playability.
Other demo CD layouts and loose demo MPQs are not automatically classified;
no retail ISO or working in-browser demo run was provided for verification.
The ISO fixture writer allocates enough 2048-byte sectors for each file so
multi-sector MPQs cannot overlap. Synthetic cross-tab Web Locks, rollback,
validation and pruning scenarios live in
port/web/tests/iso-import.test.cjs. Runtime behavior, including multi-tab
WasmFS reads and actual TFT installer fingerprints, remains unverified.
Removing local data from a running browser game
The running engine holds a shared lease for its OPFS installation generation.
Consequently, the installer cannot remove that generation while the same page
is playing. After confirmation, Remove local data sets a session-scoped
one-shot removal marker and reloads the page. Navigation shuts down the old
Emscripten worker and releases its shared generation lease. On the new page,
filesystem-bridge.js skips engine startup and installer.js asks the
installation worker to remove the game data. Success or failure clears the
marker; failed removal leaves the archives intact and displays the Web Locks
error (other game tabs still need to be closed). Normal page loads have no
removal marker and continue starting the engine. This intentionally uses a
page reload instead of attempting unsafe teardown of a live WasmFS mount.
Browser RoC/TFT selection
The engine page exposes a game-edition selector above the installer. After
validating the stored manifest, the launcher enables The Frozen Throne only if
War3x.mpq is present and the installation is not labelled as demo. The
selection is preserved in tab-scoped sessionStorage and defaults to Reign of
Chaos. Selecting a different edition reloads the document to release the
existing Web Locks generation lease and stop the current Emscripten engine.
The new engine receives +set fs_expansion 0 (RoC) or +set fs_expansion 1
(TFT) as early-startup arguments. No archive re-import is needed.
A demo or base-only installation disables TFT and resets a stale expansion preference to RoC. This only selects the existing engine's expansion mode; it does not claim complete TFT assets or gameplay compatibility.
Browser game control visibility
The browser Escape and fullscreen controls belong to #game-container, not
inside the canvas. The container creates a stacking context and keeps the
control bar above the canvas even if Emscripten adjusts canvas dimensions.
Fullscreen is requested on the container rather than the canvas, so the
controls remain visible in the browser fullscreen top layer. The page also
redirects direct canvas.requestFullscreen() calls to the container when
that method is writable. The Escape button continues to dispatch keyboard
events to the canvas and the fullscreen button can leave fullscreen mode.
Browser gameplay keyboard ownership
port/web/site/browser-keyboard.js captures browser keyboard defaults only while
the Warcraft III canvas owns focus (or its game container is fullscreen). Actual
keydown/keyup events continue through SDL/Emscripten unchanged; the capture
listener never calls stopPropagation(). Ctrl/Shift, Escape and number-group
shortcuts are handled in gameplay focus; reserved browser/OS shortcuts are not
claimed. Installer inputs and buttons retain normal keyboard behavior.
When a window loses focus, the canvas blurs, or the document is hidden, the
browser adapter emits key-up events for held Ctrl/Shift keys to avoid stuck
modifiers. While the game container is fullscreen, supported browsers may
grant the optional Keyboard Lock API for Escape and modifier keys; unavailable
or denied locks fall back to ordinary keyboard events. Fullscreen exit unlocks
keyboard capture. Escape may still be intercepted by browser fullscreen controls.
Synthetic test coverage is in port/web/tests/browser-keyboard.test.cjs.
Browser canvas visibility and post-import launch
The engine page initially hides the game canvas and its Escape/fullscreen controls.
Only filesystem-bridge.js, after validating the active OPFS manifest and
archive headers and acquiring its generation lease, makes the canvas visible
when launching the game. A successful browser import that includes War3.mpq
shows a centered Reload to play Warcraft III button instead of starting
an engine against a different in-memory generation. Reloading revalidates the
installation before showing the game. An empty installation leaves the canvas
and reload button hidden; importing only invalid or incomplete files does not
expose them. Demo installations use the same validated startup path.