summaryrefslogtreecommitdiff
path: root/os
diff options
context:
space:
mode:
authorPatric Stout <truebrain@openttd.org>2020-12-05 21:57:47 +0100
committerPatric Stout <github@truebrain.nl>2020-12-15 15:46:39 +0100
commitd15dc9f40f5a20bff452547a2dcb15231f9f969d (patch)
tree7b8d88635c048d906cbb6358007fb26055e24410 /os
parent2da07f76154d841bcfe9aaff4833144550186deb (diff)
downloadopenttd-d15dc9f40f5a20bff452547a2dcb15231f9f969d.tar.xz
Add: support for emscripten (play-OpenTTD-in-the-browser)
Emscripten compiles to WASM, which can be loaded via HTML / JavaScript. This allows you to play OpenTTD inside a browser. Co-authored-by: milek7 <me@milek7.pl>
Diffstat (limited to 'os')
-rw-r--r--os/emscripten/Dockerfile4
-rw-r--r--os/emscripten/README.md40
-rw-r--r--os/emscripten/cmake/FindLibLZMA.cmake20
-rw-r--r--os/emscripten/cmake/FindPNG.cmake7
-rw-r--r--os/emscripten/cmake/FindSDL2.cmake7
-rw-r--r--os/emscripten/cmake/FindZLIB.cmake7
-rw-r--r--os/emscripten/emsdk-liblzma.patch213
-rwxr-xr-xos/emscripten/loading.pngbin0 -> 4824 bytes
-rw-r--r--os/emscripten/pre.js93
-rw-r--r--os/emscripten/shell.html205
10 files changed, 596 insertions, 0 deletions
diff --git a/os/emscripten/Dockerfile b/os/emscripten/Dockerfile
new file mode 100644
index 000000000..1278a088f
--- /dev/null
+++ b/os/emscripten/Dockerfile
@@ -0,0 +1,4 @@
+FROM emscripten/emsdk
+
+COPY emsdk-liblzma.patch /
+RUN cd /emsdk/upstream/emscripten && patch -p1 < /emsdk-liblzma.patch
diff --git a/os/emscripten/README.md b/os/emscripten/README.md
new file mode 100644
index 000000000..4c5d7508c
--- /dev/null
+++ b/os/emscripten/README.md
@@ -0,0 +1,40 @@
+## How to build with Emscripten
+
+Building with Emscripten works with emsdk 2.0.10 and above.
+
+Currently there is no LibLZMA support upstream; for this we suggest to apply
+the provided patch in this folder to your emsdk installation.
+
+For convenience, a Dockerfile is supplied that does this patches for you
+against upstream emsdk docker. Best way to use it:
+
+Build the docker image:
+```
+ docker build -t emsdk-lzma .
+```
+
+Build the host tools first:
+```
+ mkdir build-host
+ docker run -it --rm -v $(pwd):$(pwd) -u $(id -u):$(id -g) --workdir $(pwd)/build-host emsdk-lzma cmake .. -DOPTION_TOOLS_ONLY=ON
+ docker run -it --rm -v $(pwd):$(pwd) -u $(id -u):$(id -g) --workdir $(pwd)/build-host emsdk-lzma make -j5 tools
+```
+
+Next, build the game with emscripten:
+
+```
+ mkdir build
+ docker run -it --rm -v $(pwd):$(pwd) -u $(id -u):$(id -g) --workdir $(pwd)/build emsdk-lzma emcmake cmake .. -DHOST_BINARY_DIR=$(pwd)/build-host -DCMAKE_BUILD_TYPE=RelWithDebInfo -DOPTION_USE_ASSERTS=OFF
+ docker run -it --rm -v $(pwd):$(pwd) -u $(id -u):$(id -g) --workdir $(pwd)/build emsdk-lzma emmake make -j5
+```
+
+And now you have in your build folder files like "openttd.html".
+
+To run it locally, you would have to start a local webserver, like:
+
+```
+ cd build
+ python3 -m http.server
+````
+
+Now you can play the game via http://127.0.0.1:8000/openttd.html .
diff --git a/os/emscripten/cmake/FindLibLZMA.cmake b/os/emscripten/cmake/FindLibLZMA.cmake
new file mode 100644
index 000000000..99d1ca640
--- /dev/null
+++ b/os/emscripten/cmake/FindLibLZMA.cmake
@@ -0,0 +1,20 @@
+# LibLZMA is a recent addition to the emscripten SDK, so it is possible
+# someone hasn't updated his SDK yet. Test out if the SDK supports LibLZMA.
+include(CheckCXXSourceCompiles)
+set(CMAKE_REQUIRED_FLAGS "-sUSE_LIBLZMA=1")
+
+check_cxx_source_compiles("
+ #include <lzma.h>
+ int main() { return 0; }"
+ LIBLZMA_FOUND
+)
+
+if (LIBLZMA_FOUND)
+ add_library(LibLZMA::LibLZMA INTERFACE IMPORTED)
+ set_target_properties(LibLZMA::LibLZMA PROPERTIES
+ INTERFACE_COMPILE_OPTIONS "-sUSE_LIBLZMA=1"
+ INTERFACE_LINK_LIBRARIES "-sUSE_LIBLZMA=1"
+ )
+else()
+ message(WARNING "You are using an emscripten SDK without LibLZMA support. Many savegames won't be able to load in OpenTTD. Please apply 'emsdk-liblzma.patch' to your local emsdk installation.")
+endif()
diff --git a/os/emscripten/cmake/FindPNG.cmake b/os/emscripten/cmake/FindPNG.cmake
new file mode 100644
index 000000000..2616af33d
--- /dev/null
+++ b/os/emscripten/cmake/FindPNG.cmake
@@ -0,0 +1,7 @@
+add_library(PNG::PNG INTERFACE IMPORTED)
+set_target_properties(PNG::PNG PROPERTIES
+ INTERFACE_COMPILE_OPTIONS "-sUSE_LIBPNG=1"
+ INTERFACE_LINK_LIBRARIES "-sUSE_LIBPNG=1"
+)
+
+set(PNG_FOUND on)
diff --git a/os/emscripten/cmake/FindSDL2.cmake b/os/emscripten/cmake/FindSDL2.cmake
new file mode 100644
index 000000000..54553958b
--- /dev/null
+++ b/os/emscripten/cmake/FindSDL2.cmake
@@ -0,0 +1,7 @@
+add_library(SDL2::SDL2 INTERFACE IMPORTED)
+set_target_properties(SDL2::SDL2 PROPERTIES
+ INTERFACE_COMPILE_OPTIONS "-sUSE_SDL=2"
+ INTERFACE_LINK_LIBRARIES "-sUSE_SDL=2"
+)
+
+set(SDL2_FOUND on)
diff --git a/os/emscripten/cmake/FindZLIB.cmake b/os/emscripten/cmake/FindZLIB.cmake
new file mode 100644
index 000000000..2ade2ba1b
--- /dev/null
+++ b/os/emscripten/cmake/FindZLIB.cmake
@@ -0,0 +1,7 @@
+add_library(ZLIB::ZLIB INTERFACE IMPORTED)
+set_target_properties(ZLIB::ZLIB PROPERTIES
+ INTERFACE_COMPILE_OPTIONS "-sUSE_ZLIB=1"
+ INTERFACE_LINK_LIBRARIES "-sUSE_ZLIB=1"
+)
+
+set(ZLIB_FOUND on)
diff --git a/os/emscripten/emsdk-liblzma.patch b/os/emscripten/emsdk-liblzma.patch
new file mode 100644
index 000000000..103adae0c
--- /dev/null
+++ b/os/emscripten/emsdk-liblzma.patch
@@ -0,0 +1,213 @@
+From 90dd4d4c6b1cedec338ff5b375fffca93700f7bc Mon Sep 17 00:00:00 2001
+From: milek7 <me@milek7.pl>
+Date: Tue, 8 Dec 2020 01:03:31 +0100
+Subject: [PATCH] Add liblzma port
+
+---
+Source: https://github.com/emscripten-core/emscripten/pull/12990
+
+Modifed by OpenTTD to have the bare minimum needed to work. Otherwise there
+are constantly conflicts when trying to apply this patch to different versions
+of emsdk.
+
+diff --git a/embuilder.py b/embuilder.py
+index 818262190ed..ab7d5adb7b2 100755
+--- a/embuilder.py
++++ b/embuilder.py
+@@ -60,6 +60,7 @@
+ 'harfbuzz',
+ 'icu',
+ 'libjpeg',
++ 'liblzma',
+ 'libpng',
+ 'ogg',
+ 'regal',
+@@ -197,6 +198,8 @@ def main():
+ build_port('ogg', libname('libogg'))
+ elif what == 'libjpeg':
+ build_port('libjpeg', libname('libjpeg'))
++ elif what == 'liblzma':
++ build_port('liblzma', libname('liblzma'))
+ elif what == 'libpng':
+ build_port('libpng', libname('libpng'))
+ elif what == 'sdl2':
+diff --git a/src/settings.js b/src/settings.js
+index 61cd98939ba..be6fcb678c6 100644
+--- a/src/settings.js
++++ b/src/settings.js
+@@ -1197,6 +1197,9 @@ var USE_BZIP2 = 0;
+ // 1 = use libjpeg from emscripten-ports
+ var USE_LIBJPEG = 0;
+
++// 1 = use liblzma from emscripten-ports
++var USE_LIBLZMA = 0;
++
+ // 1 = use libpng from emscripten-ports
+ var USE_LIBPNG = 0;
+
+diff --git a/tools/ports/liblzma.py b/tools/ports/liblzma.py
+new file mode 100644
+index 00000000000..e9567ef36ff
+--- /dev/null
++++ b/tools/ports/liblzma.py
+@@ -0,0 +1,160 @@
++# Copyright 2020 The Emscripten Authors. All rights reserved.
++# Emscripten is available under two separate licenses, the MIT license and the
++# University of Illinois/NCSA Open Source License. Both these licenses can be
++# found in the LICENSE file.
++
++import os
++import shutil
++
++VERSION = '5.2.5'
++HASH = '7443674247deda2935220fbc4dfc7665e5bb5a260be8ad858c8bd7d7b9f0f868f04ea45e62eb17c0a5e6a2de7c7500ad2d201e2d668c48ca29bd9eea5a73a3ce'
++
++
++def needed(settings):
++ return settings.USE_LIBLZMA
++
++
++def get(ports, settings, shared):
++ libname = ports.get_lib_name('liblzma')
++ ports.fetch_project('liblzma', 'https://tukaani.org/xz/xz-' + VERSION + '.tar.gz', 'xz-' + VERSION, sha512hash=HASH)
++
++ def create():
++ ports.clear_project_build('liblzma')
++
++ source_path = os.path.join(ports.get_dir(), 'liblzma', 'xz-' + VERSION)
++ dest_path = os.path.join(ports.get_build_dir(), 'liblzma')
++
++ shared.try_delete(dest_path)
++ os.makedirs(dest_path)
++ shutil.rmtree(dest_path, ignore_errors=True)
++ shutil.copytree(source_path, dest_path)
++
++ build_flags = ['-DHAVE_CONFIG_H', '-DTUKLIB_SYMBOL_PREFIX=lzma_', '-fvisibility=hidden']
++ exclude_dirs = ['xzdec', 'xz', 'lzmainfo']
++ exclude_files = ['crc32_small.c', 'crc64_small.c', 'crc32_tablegen.c', 'crc64_tablegen.c', 'price_tablegen.c', 'fastpos_tablegen.c'
++ 'tuklib_exit.c', 'tuklib_mbstr_fw.c', 'tuklib_mbstr_width.c', 'tuklib_open_stdxxx.c', 'tuklib_progname.c']
++ include_dirs_rel = ['../common', 'api', 'common', 'check', 'lz', 'rangecoder', 'lzma', 'delta', 'simple']
++
++ open(os.path.join(dest_path, 'src', 'config.h'), 'w').write(config_h)
++
++ final = os.path.join(dest_path, libname)
++ include_dirs = [os.path.join(dest_path, 'src', 'liblzma', p) for p in include_dirs_rel]
++ ports.build_port(os.path.join(dest_path, 'src'), final, flags=build_flags, exclude_dirs=exclude_dirs, exclude_files=exclude_files, includes=include_dirs)
++
++ ports.install_headers(os.path.join(dest_path, 'src', 'liblzma', 'api'), 'lzma.h')
++ ports.install_headers(os.path.join(dest_path, 'src', 'liblzma', 'api', 'lzma'), '*.h', 'lzma')
++
++ return final
++
++ return [shared.Cache.get(libname, create, what='port')]
++
++
++def clear(ports, settings, shared):
++ shared.Cache.erase_file(ports.get_lib_name('liblzma'))
++
++
++def process_args(ports):
++ return []
++
++
++def show():
++ return 'liblzma (USE_LIBLZMA=1; public domain)'
++
++
++config_h = r'''
++#define ASSUME_RAM 128
++#define ENABLE_NLS 1
++#define HAVE_CHECK_CRC32 1
++#define HAVE_CHECK_CRC64 1
++#define HAVE_CHECK_SHA256 1
++#define HAVE_CLOCK_GETTIME 1
++#define HAVE_DCGETTEXT 1
++#define HAVE_DECL_CLOCK_MONOTONIC 1
++#define HAVE_DECL_PROGRAM_INVOCATION_NAME 1
++#define HAVE_DECODERS 1
++#define HAVE_DECODER_ARM 1
++#define HAVE_DECODER_ARMTHUMB 1
++#define HAVE_DECODER_DELTA 1
++#define HAVE_DECODER_IA64 1
++#define HAVE_DECODER_LZMA1 1
++#define HAVE_DECODER_LZMA2 1
++#define HAVE_DECODER_POWERPC 1
++#define HAVE_DECODER_SPARC 1
++#define HAVE_DECODER_X86 1
++#define HAVE_DLFCN_H 1
++#define HAVE_ENCODERS 1
++#define HAVE_ENCODER_ARM 1
++#define HAVE_ENCODER_ARMTHUMB 1
++#define HAVE_ENCODER_DELTA 1
++#define HAVE_ENCODER_IA64 1
++#define HAVE_ENCODER_LZMA1 1
++#define HAVE_ENCODER_LZMA2 1
++#define HAVE_ENCODER_POWERPC 1
++#define HAVE_ENCODER_SPARC 1
++#define HAVE_ENCODER_X86 1
++#define HAVE_FCNTL_H 1
++#define HAVE_FUTIMENS 1
++#define HAVE_GETOPT_H 1
++#define HAVE_GETOPT_LONG 1
++#define HAVE_GETTEXT 1
++#define HAVE_IMMINTRIN_H 1
++#define HAVE_INTTYPES_H 1
++#define HAVE_LIMITS_H 1
++#define HAVE_MBRTOWC 1
++#define HAVE_MEMORY_H 1
++#define HAVE_MF_BT2 1
++#define HAVE_MF_BT3 1
++#define HAVE_MF_BT4 1
++#define HAVE_MF_HC3 1
++#define HAVE_MF_HC4 1
++#define HAVE_OPTRESET 1
++#define HAVE_POSIX_FADVISE 1
++#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1
++#define HAVE_PTHREAD_PRIO_INHERIT 1
++#define HAVE_STDBOOL_H 1
++#define HAVE_STDINT_H 1
++#define HAVE_STDLIB_H 1
++#define HAVE_STRINGS_H 1
++#define HAVE_STRING_H 1
++#define HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC 1
++#define HAVE_SYS_PARAM_H 1
++#define HAVE_SYS_STAT_H 1
++#define HAVE_SYS_TIME_H 1
++#define HAVE_SYS_TYPES_H 1
++#define HAVE_UINTPTR_T 1
++#define HAVE_UNISTD_H 1
++#define HAVE_VISIBILITY 1
++#define HAVE_WCWIDTH 1
++#define HAVE__BOOL 1
++#define HAVE___BUILTIN_ASSUME_ALIGNED 1
++#define HAVE___BUILTIN_BSWAPXX 1
++#define MYTHREAD_POSIX 1
++#define NDEBUG 1
++#define PACKAGE "xz"
++#define PACKAGE_BUGREPORT "lasse.collin@tukaani.org"
++#define PACKAGE_NAME "XZ Utils"
++#define PACKAGE_STRING "XZ Utils 5.2.5"
++#define PACKAGE_TARNAME "xz"
++#define PACKAGE_VERSION "5.2.5"
++#define SIZEOF_SIZE_T 4
++#define STDC_HEADERS 1
++#define TUKLIB_CPUCORES_SYSCONF 1
++#define TUKLIB_FAST_UNALIGNED_ACCESS 1
++#define TUKLIB_PHYSMEM_SYSCONF 1
++#ifndef _ALL_SOURCE
++# define _ALL_SOURCE 1
++#endif
++#ifndef _GNU_SOURCE
++# define _GNU_SOURCE 1
++#endif
++#ifndef _POSIX_PTHREAD_SEMANTICS
++# define _POSIX_PTHREAD_SEMANTICS 1
++#endif
++#ifndef _TANDEM_SOURCE
++# define _TANDEM_SOURCE 1
++#endif
++#ifndef __EXTENSIONS__
++# define __EXTENSIONS__ 1
++#endif
++#define VERSION "5.2.5"
++'''
diff --git a/os/emscripten/loading.png b/os/emscripten/loading.png
new file mode 100755
index 000000000..d4c4aaf75
--- /dev/null
+++ b/os/emscripten/loading.png
Binary files differ
diff --git a/os/emscripten/pre.js b/os/emscripten/pre.js
new file mode 100644
index 000000000..5cbd899e0
--- /dev/null
+++ b/os/emscripten/pre.js
@@ -0,0 +1,93 @@
+Module.arguments.push('-mnull', '-snull', '-vsdl:relative_mode');
+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") {
+ return "wss://content.openttd.org/";
+ }
+
+ /* Everything else just tries to make a default WebSocket connection.
+ * If you run your own server you can setup your own WebSocket proxy in
+ * front of it and let people connect to your server via the proxy. You
+ * are best to add another "if" statement as above for this. */
+ return null;
+} };
+
+Module.preRun.push(function() {
+ personal_dir = '/home/web_user/.openttd';
+ content_download_dir = personal_dir + '/content_download'
+
+ /* Because of the "-c" above, all user-data is stored in /user_data. */
+ FS.mkdir(personal_dir);
+ FS.mount(IDBFS, {}, personal_dir);
+
+ Module.addRunDependency('syncfs');
+ FS.syncfs(true, function (err) {
+ /* FS.mkdir() tends to fail if parent folders do not exist. */
+ if (!FS.analyzePath(content_download_dir).exists) {
+ FS.mkdir(content_download_dir);
+ }
+ if (!FS.analyzePath(content_download_dir + '/baseset').exists) {
+ FS.mkdir(content_download_dir + '/baseset');
+ }
+
+ /* Check if the OpenGFX baseset is already downloaded. */
+ if (!FS.analyzePath(content_download_dir + '/baseset/opengfx-0.6.0.tar').exists) {
+ window.openttd_downloaded_opengfx = true;
+ FS.createPreloadedFile(content_download_dir + '/baseset', 'opengfx-0.6.0.tar', 'https://installer.cdn.openttd.org/emscripten/opengfx-0.6.0.tar', true, true);
+ } else {
+ /* Fake dependency increase, so the counter is stable. */
+ Module.addRunDependency('opengfx');
+ Module.removeRunDependency('opengfx');
+ }
+
+ Module.removeRunDependency('syncfs');
+ });
+
+ window.openttd_syncfs_shown_warning = false;
+ window.openttd_syncfs = function() {
+ /* Copy the virtual FS to the persistent storage. */
+ FS.syncfs(false, function (err) { });
+
+ /* On first time, warn the user about the volatile behaviour of
+ * persistent storage. */
+ if (!window.openttd_syncfs_shown_warning) {
+ window.openttd_syncfs_shown_warning = true;
+ Module.onWarningFs();
+ }
+ }
+
+ window.openttd_exit = function() {
+ Module.onExit();
+ }
+
+ window.openttd_abort = function() {
+ Module.onAbort();
+ }
+
+ window.openttd_server_list = function() {
+ add_server = Module.cwrap("em_openttd_add_server", null, ["string", "number"]);
+
+ /* Add servers that support WebSocket here. Example:
+ * add_server("localhost", 3979); */
+ }
+
+ /* https://github.com/emscripten-core/emscripten/pull/12995 implements this
+ * properly. Till that time, we use a polyfill. */
+ SOCKFS.websocket_sock_ops.createPeer_ = SOCKFS.websocket_sock_ops.createPeer;
+ SOCKFS.websocket_sock_ops.createPeer = function(sock, addr, port)
+ {
+ let func = Module['websocket']['url'];
+ Module['websocket']['url'] = func(addr, port, (sock.type == 2) ? 'udp' : 'tcp');
+ let ret = SOCKFS.websocket_sock_ops.createPeer_(sock, addr, port);
+ Module['websocket']['url'] = func;
+ return ret;
+ }
+});
+
+Module.postRun.push(function() {
+ /* Check if we downloaded OpenGFX; if so, sync the virtual FS back to the
+ * IDBFS so OpenGFX is stored persistent. */
+ if (window['openttd_downloaded_opengfx']) {
+ FS.syncfs(false, function (err) { });
+ }
+});
diff --git a/os/emscripten/shell.html b/os/emscripten/shell.html
new file mode 100644
index 000000000..17ea5b414
--- /dev/null
+++ b/os/emscripten/shell.html
@@ -0,0 +1,205 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>OpenTTD</title>
+ <style>
+ body {
+ font-family: Tahoma, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ margin: 0;
+ padding: 0;
+ }
+
+ div.background {
+ background-image: url("data:image/png;base64,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");
+ border: 0px none;
+ display: flex;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ z-index: 1;
+ }
+
+ div.overlay {
+ height: 40px;
+ margin: 0 auto;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ text-align: center;
+ transition: opacity 0.3s;
+ width: 100%;
+ z-index: 3;
+ }
+ div.overlay > div {
+ pointer-events: none;
+ }
+
+ div.background > div, div.overlay > div {
+ margin: auto;
+ }
+
+ #filesystem {
+ background-color: #e00000;
+ border: 1px solid #fc6458;
+ color: #fcf880;
+ display: none;
+ outline: #a00000;
+ padding: 0 4px;
+ width: 600px;
+ }
+
+ #title, #message {
+ background-color: #838383;
+ border: 1px solid #a8a8a8;
+ outline: 1px solid #626262;
+ padding: 0 4px;
+ min-width: 260px;
+ }
+
+ #box.error #title, #box.error #message {
+ background-color: #e00000;
+ border: 1px solid #fc6458;
+ outline: #a00000;
+ }
+ #box.error #message {
+ color: #fcf880;
+ }
+
+ #title {
+ color: #fcfcfc;
+ height: 20px;
+ text-shadow: 1px 1px #101010;
+ }
+ #message {
+ color: #101010;
+ height: 54px;
+ padding: 4px 4px;
+ }
+
+ canvas.emscripten {
+ border: 0px none;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ z-index: 2;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="background">
+ <div id="box">
+ <div id="title">
+ Loading ...
+ </div>
+ <div id="message">
+ </div>
+ </div>
+ </div>
+ <div class="overlay" id="overlay">
+ <div id="filesystem">
+ Warning: savegames are stored in the Indexed DB of your browser.<br/>Your browser can delete savegames without notice!
+ </div>
+ </div>
+ <div>
+ <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
+ </div>
+
+ <script type='text/javascript'>
+ var statusElement = document.getElementById('status');
+ var progressElement = document.getElementById('progress');
+ var spinnerElement = document.getElementById('spinner');
+
+ var Module = {
+ preRun: [],
+ postRun: [],
+ arguments: [],
+ totalDependencies: 42,
+ doneDependencies: 0,
+ lastDependencies: 1,
+
+ print: function(text) {
+ if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ console.log(text);
+ },
+
+ printErr: function(text) {
+ if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ console.error(text);
+ },
+
+ canvas: (function() {
+ var canvas = document.getElementById('canvas');
+
+ // As a default initial behavior, pop up an alert when webgl context is lost. To make your
+ // application robust, you may want to override this behavior before shipping!
+ // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
+ canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
+
+ return canvas;
+ })(),
+
+ setStatus: function(text) {
+ var m = text.match(/^([^(]+)\((\d+(\.\d+)?)\/(\d+)\)$/);
+
+ if (m) {
+ text = "(" + m[2] + " / " + m[4] + ") " + m[1];
+ }
+
+ document.getElementById("message").innerHTML = text;
+ },
+
+ monitorRunDependencies: function(left) {
+ /* If it goes up, a new dependency was added; down means one is
+ * removed. We only track the latter. */
+ if (left < Module.lastDependencies) {
+ Module.doneDependencies += 1;
+ }
+ Module.lastDependencies = left;
+
+ total = Module.totalDependencies;
+ doing = Module.doneDependencies + 1;
+ if (doing > total) {
+ doing = total;
+ }
+
+ document.getElementById("title").innerHTML = "(" + doing + " / " + total + ") Loading ...";
+ document.getElementById("message").innerHTML = "Preparing game ...";
+ },
+
+ onExit: function() {
+ document.getElementById("canvas").style.display = "none";
+
+ document.getElementById("title").innerHTML = "Thank you for playing!";
+ document.getElementById("message").innerHTML = "We hope you enjoyed OpenTTD!<br/><br/>Reload your browser to restart the game.";
+ },
+
+ onAbort: function() {
+ document.getElementById("canvas").style.display = "none";
+
+ document.getElementById("box").className = "error";
+ document.getElementById("title").innerHTML = "Crash :(";
+ document.getElementById("message").innerHTML = "The game crashed!<br/><br/>Please reload your browser to restart the game.";
+ },
+
+ onWarningFs: function() {
+ document.getElementById("filesystem").style.display = "inline-block";
+ document.getElementById("overlay").style.opacity = 1;
+ setTimeout(function() {
+ document.getElementById("overlay").style.opacity = 0;
+ setTimeout(function() {
+ document.getElementById("filesystem").style.display = "none";
+ }, 300);
+ }, 10000);
+ }
+ };
+
+ window.onerror = function() {
+ Module.onAbort();
+ };
+ </script>
+ {{{ SCRIPT }}}
+ </body>
+</html>