# Source and reproducible build

This is OpenTTD 15.3 compiled to WebAssembly from the upstream C++ engine. The browser host does not recreate any simulation rules: it loads the compiled engine, presents its SDL canvas, provides touch and accessibility affordances, and prepares the engine’s packaged data.

## Exact source and inputs

| Component | Exact source | Redistributed input | SHA-256 |
| --- | --- | --- | --- |
| OpenTTD 15.3 | [`14ec60f248547d4d062a1160f0fc26d742319888`](https://github.com/OpenTTD/OpenTTD/commit/14ec60f248547d4d062a1160f0fc26d742319888) | [`openttd-15.3-source.tar.xz`](https://cdn.openttd.org/openttd-releases/15.3/openttd-15.3-source.tar.xz) | `5ea21eea7d59c78a42071924ac18c6bc0116088f2e96b14cfee9369175973be7` |
| OpenGFX 8.0 | [`e68bd9b7cb60305aa98c687b6836e0e0b08e2847`](https://github.com/OpenTTD/OpenGFX/commit/e68bd9b7cb60305aa98c687b6836e0e0b08e2847) | [`opengfx-8.0-all.zip`](https://cdn.openttd.org/opengfx-releases/8.0/opengfx-8.0-all.zip) | `43a0c1dabf39cb865394f3a6cc36d4da5c10ecfaaf55652043104806810903be` |
| OpenSFX 1.0.3 | [`4332d7f216eece2e6417f284b00afb832126433d`](https://github.com/OpenTTD/OpenSFX/commit/4332d7f216eece2e6417f284b00afb832126433d) | [`opensfx-1.0.3-all.zip`](https://cdn.openttd.org/opensfx-releases/1.0.3/opensfx-1.0.3-all.zip) | `e0a218b7dd9438e701503b0f84c25a97c1c11b7c2f025323fb19d6db16ef3759` |

The inner archives embedded in the virtual filesystem are:

- `opengfx-8.0.tar`: `9389bcb0807058c80bd95121e978f05d9ef86b4b1bc3ac2da8da8bb02456043c`
- `opensfx-1.0.3.tar`: `531a243c5f0742e34d53704263302bbb847a3dc1e618831097ee940088b7b879`

The complete modified engine tree and browser-host source are retained in the handoff workspace at `workspace/openttd/` and `workspace/host/`. The official source archive above, the two verified base-set archives, and the complete patch below reconstruct the engine source used for this binary. When redistributing the executable publicly, make that corresponding source available from the same distribution point.

## Engine modifications

The engine changes are deliberately narrow:

1. Embed the verified OpenGFX and OpenSFX tar files under `/baseset`, eliminating the first-run graphics download.
2. Keep music disabled because this target has no browser MIDI synthesizer, but allow the compiled SDL sound driver to play OpenSFX after a user gesture.
3. Report IndexedDB load and write failures to the host while still allowing a playable non-persistent session.
4. Recover transient zero-size SDL resize events from the visible browser canvas dimensions instead of collapsing the game surface.

Apply this patch to the exact OpenTTD commit above:

```diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2891bee..1aa0d98 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -358,6 +358,17 @@ endif()
 if(EMSCRIPTEN)
     add_library(WASM::WASM INTERFACE IMPORTED)
 
+    # Modified 2026-08-11 for the portable browser build. Keep the openly
+    # licensed base graphics and sounds inside the Emscripten filesystem so
+    # first launch never depends on OpenTTD's remote content service.
+    set(BROWSER_OPENGFX_TAR "${CMAKE_SOURCE_DIR}/browser-assets/baseset/opengfx-8.0.tar")
+    set(BROWSER_OPENSFX_TAR "${CMAKE_SOURCE_DIR}/browser-assets/baseset/opensfx-1.0.3.tar")
+    foreach(BROWSER_BASESET_TAR IN ITEMS "${BROWSER_OPENGFX_TAR}" "${BROWSER_OPENSFX_TAR}")
+        if(NOT EXISTS "${BROWSER_BASESET_TAR}")
+            message(FATAL_ERROR "Required redistributable browser base set is missing: ${BROWSER_BASESET_TAR}")
+        endif()
+    endforeach()
+
     # Allow heap-growth, and start with a bigger memory size.
     target_link_libraries(WASM::WASM INTERFACE "-s ALLOW_MEMORY_GROWTH=1")
     target_link_libraries(WASM::WASM INTERFACE "-s INITIAL_MEMORY=33554432")
@@ -375,6 +386,8 @@ if(EMSCRIPTEN)
     # the more languages you add the slower downloading becomes, we decided to
     # only ship the English language.
     target_link_libraries(WASM::WASM INTERFACE "--preload-file ${CMAKE_BINARY_DIR}/baseset@/baseset")
+    target_link_libraries(WASM::WASM INTERFACE "--preload-file ${BROWSER_OPENGFX_TAR}@/baseset/opengfx-8.0.tar")
+    target_link_libraries(WASM::WASM INTERFACE "--preload-file ${BROWSER_OPENSFX_TAR}@/baseset/opensfx-1.0.3.tar")
     target_link_libraries(WASM::WASM INTERFACE "--preload-file ${CMAKE_BINARY_DIR}/lang/english.lng@/lang/english.lng")
     target_link_libraries(WASM::WASM INTERFACE "--preload-file ${CMAKE_SOURCE_DIR}/bin/ai@/ai")
     target_link_libraries(WASM::WASM INTERFACE "--preload-file ${CMAKE_SOURCE_DIR}/bin/game@/game")
diff --git a/os/emscripten/pre.js b/os/emscripten/pre.js
index 8e46ae3..921fcc9 100644
--- a/os/emscripten/pre.js
+++ b/os/emscripten/pre.js
@@ -1,4 +1,7 @@
-Module.arguments.push('-mnull', '-snull', '-vsdl');
+/* Modified 2026-08-11 for the portable browser build. OpenSFX is bundled and
+ * SDL's Web Audio driver owns sound effects; music remains disabled because
+ * this Emscripten target has no browser MIDI synthesizer. */
+Module.arguments.push('-mnull', '-vsdl');
 Module['websocket'] = { url: function(host, port, proto) {
     /* openttd.org hosts a WebSocket proxy for the content service. */
     if (host == "content.openttd.org" && port == 3978 && proto == "tcp") {
@@ -30,6 +33,9 @@ Module.preRun.push(function() {
 
     Module.addRunDependency('syncfs');
     FS.syncfs(true, function (err) {
+        if (err && Module.onPersistenceError) {
+            Module.onPersistenceError('load', err);
+        }
         Module.removeRunDependency('syncfs');
     });
 
@@ -37,6 +43,9 @@ Module.preRun.push(function() {
     window.openttd_syncfs = function(callback) {
         /* Copy the virtual FS to the persistent storage. */
         FS.syncfs(false, function (err) {
+            if (err && Module.onPersistenceError) {
+                Module.onPersistenceError('save', err);
+            }
             /* On first time, warn the user about the volatile behaviour of
              * persistent storage. */
             if (!window.openttd_syncfs_shown_warning) {
diff --git a/src/video/sdl2_v.cpp b/src/video/sdl2_v.cpp
index e31afce..75e039e 100644
--- a/src/video/sdl2_v.cpp
+++ b/src/video/sdl2_v.cpp
@@ -508,8 +508,24 @@ bool VideoDriver_SDL_Base::PollEvent()
 				/* Force a redraw of the entire screen. */
 				this->MakeDirty(0, 0, _screen.width, _screen.height);
 			} else if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
-				int w = std::max(ev.window.data1, 64);
-				int h = std::max(ev.window.data2, 64);
+				int w = ev.window.data1;
+				int h = ev.window.data2;
+#ifdef __EMSCRIPTEN__
+				/* Modified 2026-08-11 for the browser host. SDL can emit a
+				 * transient 0x0 size event while the canvas shell changes state.
+				 * Use the visible canvas size instead of collapsing the game to
+				 * OpenTTD's 64x64 safety minimum. */
+				if (w <= 0 || h <= 0) {
+					double css_width = 0;
+					double css_height = 0;
+					if (emscripten_get_element_css_size("#game-canvas", &css_width, &css_height) == EMSCRIPTEN_RESULT_SUCCESS) {
+						w = static_cast<int>(css_width);
+						h = static_cast<int>(css_height);
+					}
+				}
+#endif
+				w = std::max(w, 64);
+				h = std::max(h, 64);
 				CreateMainSurface(w, h, w != ev.window.data1 || h != ev.window.data2);
 			} else if (ev.window.event == SDL_WINDOWEVENT_ENTER) {
 				/* mouse entered the window, enable cursor */
```

## Reproducible engine build

OpenTTD 15.3 pins `emscripten/emsdk:3.1.57`. The image used here resolved to `sha256:8b7c9e9e95f3fb92b94876727a35235a8d2908c4d7e2ef2427f78366fd0b1130` and includes the custom liblzma port needed for compressed savegames.

```sh
docker build --platform linux/amd64 -t emsdk-openttd-15.3 os/emscripten

mkdir -p build-host build-wasm

docker run --platform linux/amd64 --rm -v "$PWD:$PWD" -u "$(id -u):$(id -g)" \
  --workdir "$PWD/build-host" emsdk-openttd-15.3 \
  cmake .. -DOPTION_TOOLS_ONLY=ON -DCMAKE_BUILD_TYPE=Release

docker run --platform linux/amd64 --rm -v "$PWD:$PWD" -u "$(id -u):$(id -g)" \
  --workdir "$PWD/build-host" emsdk-openttd-15.3 \
  cmake --build . --target tools --parallel 6

docker run --platform linux/amd64 --rm -v "$PWD:$PWD" -u "$(id -u):$(id -g)" \
  --workdir "$PWD/build-wasm" emsdk-openttd-15.3 \
  emcmake cmake .. -DHOST_BINARY_DIR=../build-host -DCMAKE_BUILD_TYPE=Release -DOPTION_USE_ASSERTS=OFF

docker run --platform linux/amd64 --rm -v "$PWD:$PWD" -u "$(id -u):$(id -g)" \
  --workdir "$PWD/build-wasm" emsdk-openttd-15.3 \
  cmake --build . --target openttd --parallel 6
```

The browser host is strict TypeScript built with TypeScript 5.9 and esbuild 0.25. Its `npm run check` command type-checks both browser and service-worker targets, runs deterministic manifest/chunk tests, and emits the static shell. `npm run package` then splits the unchanged engine `.wasm` and `.data` byte streams into sub-5 MiB files; the browser reassembles and verifies them before Emscripten starts. Exact payload and per-chunk SHA-256 values are in [engine/payload-manifest.json](engine/payload-manifest.json). [SOURCE-MANIFEST.json](SOURCE-MANIFEST.json) fingerprints the compiled engine script, original Wasm and data streams, browser host, sealed prompt, source revisions, open assets, and toolchain.

## Licence files

- [OpenTTD GPL-2.0](licenses/OPENTTD-GPL-2.0.md)
- [OpenTTD third-party notices](licenses/OPENTTD-README.md)
- [OpenGFX GPL-2.0 and credits](licenses/OPENGFX-README.txt)
- [OpenGFX sprite-by-sprite artist ledger](licenses/OPENGFX-AUTHORS.csv)
- [OpenSFX licences](licenses/OPENSFX-LICENSE.txt)
- [OpenSFX sound-by-sound attributions](licenses/OPENSFX-README.txt)
