summaryrefslogtreecommitdiff
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
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>
-rw-r--r--.github/workflows/ci-build.yml49
-rw-r--r--CMakeLists.txt37
-rw-r--r--cmake/Options.cmake8
-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
-rw-r--r--src/fontcache.cpp3
-rw-r--r--src/gfx_type.h8
-rw-r--r--src/ini.cpp7
-rw-r--r--src/network/core/address.cpp10
-rw-r--r--src/network/core/os_abstraction.h24
-rw-r--r--src/network/network.cpp11
-rw-r--r--src/network/network_content.cpp15
-rw-r--r--src/network/network_gui.cpp17
-rw-r--r--src/openttd.cpp14
-rw-r--r--src/saveload/saveload.cpp7
-rw-r--r--src/video/sdl2_v.cpp45
-rw-r--r--src/video/sdl2_v.h5
25 files changed, 844 insertions, 12 deletions
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 31d0b511d..8794b79da 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -10,6 +10,55 @@ env:
CTEST_OUTPUT_ON_FAILURE: 1
jobs:
+ emscripten:
+ name: Emscripten
+
+ runs-on: ubuntu-20.04
+ container:
+ # If you change this version, change the number in the cache step too.
+ image: emscripten/emsdk:2.0.10
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup cache
+ uses: actions/cache@v2
+ with:
+ path: /emsdk/upstream/emscripten/cache
+ key: 2.0.10-${{ runner.os }}
+
+ - name: Build (host tools)
+ run: |
+ mkdir build-host
+ cd build-host
+
+ echo "::group::CMake"
+ cmake .. -DOPTION_TOOLS_ONLY=ON
+ echo "::endgroup::"
+
+ echo "::group::Build"
+ echo "Running on $(nproc) cores"
+ make -j$(nproc) tools
+ echo "::endgroup::"
+
+ - name: Install GCC problem matcher
+ uses: ammaraskar/gcc-problem-matcher@master
+
+ - name: Build
+ run: |
+ mkdir build
+ cd build
+
+ echo "::group::CMake"
+ emcmake cmake .. -DHOST_BINARY_DIR=../build-host
+ echo "::endgroup::"
+
+ echo "::group::Build"
+ echo "Running on $(nproc) cores"
+ emmake make -j$(nproc)
+ echo "::endgroup::"
+
linux:
name: Linux
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 15c6e03c0..ca2284ffc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,6 +15,10 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
+if (EMSCRIPTEN)
+ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/os/emscripten/cmake")
+endif()
+
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9)
@@ -232,6 +236,39 @@ if(APPLE)
)
endif()
+if(EMSCRIPTEN)
+ add_library(WASM::WASM INTERFACE IMPORTED)
+
+ # 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")
+
+ # Export functions to Javascript.
+ target_link_libraries(WASM::WASM INTERFACE "-s EXPORTED_FUNCTIONS='[\"_main\", \"_em_openttd_add_server\"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]'")
+
+ # Preload all the files we generate during build.
+ # As we do not compile with FreeType / FontConfig, we also have no way to
+ # render several languages (like Chinese, ..), so where do you draw the
+ # line what languages to include and which not? In the end, especially as
+ # 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 ${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")
+
+ # We use IDBFS for persistent storage.
+ target_link_libraries(WASM::WASM INTERFACE "-lidbfs.js")
+
+ # Use custom pre-js and shell.html.
+ target_link_libraries(WASM::WASM INTERFACE "--pre-js ${CMAKE_SOURCE_DIR}/os/emscripten/pre.js")
+ target_link_libraries(WASM::WASM INTERFACE "--shell-file ${CMAKE_SOURCE_DIR}/os/emscripten/shell.html")
+
+ # Build the .html (which builds the .js, .wasm, and .data too).
+ set_target_properties(openttd PROPERTIES SUFFIX ".html")
+ target_link_libraries(openttd WASM::WASM)
+endif()
+
if(NOT PERSONAL_DIR STREQUAL "(not set)")
add_definitions(
-DWITH_PERSONAL_DIR
diff --git a/cmake/Options.cmake b/cmake/Options.cmake
index 977d54f06..c94a193b3 100644
--- a/cmake/Options.cmake
+++ b/cmake/Options.cmake
@@ -55,7 +55,13 @@ function(set_options)
option(OPTION_DEDICATED "Build dedicated server only (no GUI)" OFF)
option(OPTION_INSTALL_FHS "Install with Filesystem Hierarchy Standard folders" ${DEFAULT_OPTION_INSTALL_FHS})
option(OPTION_USE_ASSERTS "Use assertions; leave enabled for nightlies, betas, and RCs" ON)
- option(OPTION_USE_THREADS "Use threads" ON)
+ if(EMSCRIPTEN)
+ # Although pthreads is supported, it is not in a way yet that is
+ # useful for us.
+ option(OPTION_USE_THREADS "Use threads" OFF)
+ else()
+ option(OPTION_USE_THREADS "Use threads" ON)
+ endif()
option(OPTION_USE_NSIS "Use NSIS to create windows installer; enable only for stable releases" OFF)
option(OPTION_TOOLS_ONLY "Build only tools target" OFF)
option(OPTION_DOCS_ONLY "Build only docs target" OFF)
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>
diff --git a/src/fontcache.cpp b/src/fontcache.cpp
index 051c4ee1d..79683d193 100644
--- a/src/fontcache.cpp
+++ b/src/fontcache.cpp
@@ -26,7 +26,6 @@
#include "safeguards.h"
static const int ASCII_LETTERSTART = 32; ///< First printable ASCII letter.
-static const int MAX_FONT_SIZE = 72; ///< Maximum font size.
/** Default heights for the different sizes of fonts. */
static const int _default_font_height[FS_END] = {10, 6, 18, 10};
@@ -202,6 +201,8 @@ bool SpriteFontCache::GetDrawGlyphShadow()
FreeTypeSettings _freetype;
+static const int MAX_FONT_SIZE = 72; ///< Maximum font size.
+
static const byte FACE_COLOUR = 1;
static const byte SHADOW_COLOUR = 2;
diff --git a/src/gfx_type.h b/src/gfx_type.h
index 6fca2228d..452bc2c7e 100644
--- a/src/gfx_type.h
+++ b/src/gfx_type.h
@@ -162,7 +162,9 @@ struct DrawPixelInfo {
union Colour {
uint32 data; ///< Conversion of the channel information to a 32 bit number.
struct {
-#if TTD_ENDIAN == TTD_BIG_ENDIAN
+#if defined(__EMSCRIPTEN__)
+ uint8 r, g, b, a; ///< colour channels as used in browsers
+#elif TTD_ENDIAN == TTD_BIG_ENDIAN
uint8 a, r, g, b; ///< colour channels in BE order
#else
uint8 b, g, r, a; ///< colour channels in LE order
@@ -177,7 +179,9 @@ union Colour {
* @param a The channel for the alpha/transparency.
*/
Colour(uint8 r, uint8 g, uint8 b, uint8 a = 0xFF) :
-#if TTD_ENDIAN == TTD_BIG_ENDIAN
+#if defined(__EMSCRIPTEN__)
+ r(r), g(g), b(b), a(a)
+#elif TTD_ENDIAN == TTD_BIG_ENDIAN
a(a), r(r), g(g), b(b)
#else
b(b), g(g), r(r), a(a)
diff --git a/src/ini.cpp b/src/ini.cpp
index 6948bc1ea..fc9b1e8fd 100644
--- a/src/ini.cpp
+++ b/src/ini.cpp
@@ -13,6 +13,9 @@
#include "string_func.h"
#include "fileio_func.h"
#include <fstream>
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+#endif
#if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 500)
# include <unistd.h>
@@ -115,6 +118,10 @@ bool IniFile::SaveToDisk(const char *filename)
}
#endif
+#ifdef __EMSCRIPTEN__
+ EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
+#endif
+
return true;
}
diff --git a/src/network/core/address.cpp b/src/network/core/address.cpp
index c2fecc7ff..1aaa0b5fb 100644
--- a/src/network/core/address.cpp
+++ b/src/network/core/address.cpp
@@ -299,7 +299,15 @@ static SOCKET ConnectLoopProc(addrinfo *runp)
if (!SetNoDelay(sock)) DEBUG(net, 1, "[%s] setting TCP_NODELAY failed", type);
- if (connect(sock, runp->ai_addr, (int)runp->ai_addrlen) != 0) {
+ int err = connect(sock, runp->ai_addr, (int)runp->ai_addrlen);
+#ifdef __EMSCRIPTEN__
+ /* Emscripten is asynchronous, and as such a connect() is still in
+ * progress by the time the call returns. */
+ if (err != 0 && errno != EINPROGRESS)
+#else
+ if (err != 0)
+#endif
+ {
DEBUG(net, 1, "[%s] could not connect %s socket: %s", type, family, strerror(errno));
closesocket(sock);
return INVALID_SOCKET;
diff --git a/src/network/core/os_abstraction.h b/src/network/core/os_abstraction.h
index 01ab68b27..be8b8f919 100644
--- a/src/network/core/os_abstraction.h
+++ b/src/network/core/os_abstraction.h
@@ -83,6 +83,16 @@ typedef unsigned long in_addr_t;
# include <errno.h>
# include <sys/time.h>
# include <netdb.h>
+
+# if defined(__EMSCRIPTEN__)
+/* Emscripten doesn't support AI_ADDRCONFIG and errors out on it. */
+# undef AI_ADDRCONFIG
+# define AI_ADDRCONFIG 0
+/* Emscripten says it supports FD_SETSIZE fds, but it really only supports 64.
+ * https://github.com/emscripten-core/emscripten/issues/1711 */
+# undef FD_SETSIZE
+# define FD_SETSIZE 64
+# endif
#endif /* UNIX */
/* OS/2 stuff */
@@ -148,12 +158,16 @@ typedef unsigned long in_addr_t;
*/
static inline bool SetNonBlocking(SOCKET d)
{
-#ifdef _WIN32
- u_long nonblocking = 1;
+#ifdef __EMSCRIPTEN__
+ return true;
#else
+# ifdef _WIN32
+ u_long nonblocking = 1;
+# else
int nonblocking = 1;
-#endif
+# endif
return ioctlsocket(d, FIONBIO, &nonblocking) == 0;
+#endif
}
/**
@@ -163,10 +177,14 @@ static inline bool SetNonBlocking(SOCKET d)
*/
static inline bool SetNoDelay(SOCKET d)
{
+#ifdef __EMSCRIPTEN__
+ return true;
+#else
/* XXX should this be done at all? */
int b = 1;
/* The (const char*) cast is needed for windows */
return setsockopt(d, IPPROTO_TCP, TCP_NODELAY, (const char*)&b, sizeof(b)) == 0;
+#endif
}
/* Make sure these structures have the size we expect them to be */
diff --git a/src/network/network.cpp b/src/network/network.cpp
index 0e3d08630..a100b6b95 100644
--- a/src/network/network.cpp
+++ b/src/network/network.cpp
@@ -1154,3 +1154,14 @@ bool IsNetworkCompatibleVersion(const char *other)
const char *hash2 = ExtractNetworkRevisionHash(other);
return hash1 && hash2 && (strncmp(hash1, hash2, GITHASH_SUFFIX_LEN) == 0);
}
+
+#ifdef __EMSCRIPTEN__
+extern "C" {
+
+void CDECL em_openttd_add_server(const char *host, int port)
+{
+ NetworkUDPQueryServer(NetworkAddress(host, port), true);
+}
+
+}
+#endif
diff --git a/src/network/network_content.cpp b/src/network/network_content.cpp
index 5e401d3e9..0140d3ef2 100644
--- a/src/network/network_content.cpp
+++ b/src/network/network_content.cpp
@@ -23,6 +23,10 @@
#include <zlib.h>
#endif
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+#endif
+
#include "../safeguards.h"
extern bool HasScenario(const ContentInfo *ci, bool md5sum);
@@ -289,6 +293,13 @@ void ClientNetworkContentSocketHandler::DownloadSelectedContent(uint &files, uin
{
bytes = 0;
+#ifdef __EMSCRIPTEN__
+ /* Emscripten is loaded via an HTTPS connection. As such, it is very
+ * difficult to make HTTP connections. So always use the TCP method of
+ * downloading content. */
+ fallback = true;
+#endif
+
ContentIDList content;
for (const ContentInfo *ci : this->infos) {
if (!ci->IsSelected() || ci->state == ContentInfo::ALREADY_HERE) continue;
@@ -535,6 +546,10 @@ void ClientNetworkContentSocketHandler::AfterDownload()
unlink(GetFullFilename(this->curInfo, false));
}
+#ifdef __EMSCRIPTEN__
+ EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
+#endif
+
this->OnDownloadComplete(this->curInfo->id);
} else {
ShowErrorMessage(STR_CONTENT_ERROR_COULD_NOT_EXTRACT, INVALID_STRING_ID, WL_ERROR);
diff --git a/src/network/network_gui.cpp b/src/network/network_gui.cpp
index c430c47e5..47bf8fb69 100644
--- a/src/network/network_gui.cpp
+++ b/src/network/network_gui.cpp
@@ -39,6 +39,9 @@
#include "../safeguards.h"
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+#endif
static void ShowNetworkStartServerWindow();
static void ShowNetworkLobbyWindow(NetworkGameList *ngl);
@@ -475,6 +478,14 @@ public:
this->filter_editbox.cancel_button = QueryString::ACTION_CLEAR;
this->SetFocusedWidget(WID_NG_FILTER);
+ /* As the master-server doesn't support "websocket" servers yet, we
+ * let "os/emscripten/pre.js" hardcode a list of servers people can
+ * join. This means the serverlist is curated for now, but it is the
+ * best we can offer. */
+#ifdef __EMSCRIPTEN__
+ EM_ASM(if (window["openttd_server_list"]) openttd_server_list());
+#endif
+
this->last_joined = NetworkGameListAddItem(NetworkAddress(_settings_client.network.last_host, _settings_client.network.last_port));
this->server = this->last_joined;
if (this->last_joined != nullptr) NetworkUDPQueryServer(this->last_joined->address);
@@ -615,6 +626,12 @@ public:
this->GetWidget<NWidgetStacked>(WID_NG_NEWGRF_SEL)->SetDisplayedPlane(sel == nullptr || !sel->online || sel->info.grfconfig == nullptr);
this->GetWidget<NWidgetStacked>(WID_NG_NEWGRF_MISSING_SEL)->SetDisplayedPlane(sel == nullptr || !sel->online || sel->info.grfconfig == nullptr || !sel->info.version_compatible || sel->info.compatible);
+#ifdef __EMSCRIPTEN__
+ this->SetWidgetDisabledState(WID_NG_FIND, true);
+ this->SetWidgetDisabledState(WID_NG_ADD, true);
+ this->SetWidgetDisabledState(WID_NG_START, true);
+#endif
+
this->DrawWidgets();
}
diff --git a/src/openttd.cpp b/src/openttd.cpp
index 4aeed3928..9cc5d2f20 100644
--- a/src/openttd.cpp
+++ b/src/openttd.cpp
@@ -73,6 +73,11 @@
#include "safeguards.h"
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+# include <emscripten/html5.h>
+#endif
+
void CallLandscapeTick();
void IncreaseDate();
void DoPaletteAnimations();
@@ -104,6 +109,15 @@ void CDECL usererror(const char *s, ...)
ShowOSErrorBox(buf, false);
if (VideoDriver::GetInstance() != nullptr) VideoDriver::GetInstance()->Stop();
+#ifdef __EMSCRIPTEN__
+ emscripten_exit_pointerlock();
+ /* In effect, the game ends here. As emscripten_set_main_loop() caused
+ * the stack to be unwound, the code after MainLoop() in
+ * openttd_main() is never executed. */
+ EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
+ EM_ASM(if (window["openttd_abort"]) openttd_abort());
+#endif
+
exit(1);
}
diff --git a/src/saveload/saveload.cpp b/src/saveload/saveload.cpp
index 999a381d7..5288f9047 100644
--- a/src/saveload/saveload.cpp
+++ b/src/saveload/saveload.cpp
@@ -45,6 +45,9 @@
#include "../error.h"
#include <atomic>
#include <string>
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+#endif
#include "table/strings.h"
@@ -2495,6 +2498,10 @@ static void SaveFileDone()
InvalidateWindowData(WC_STATUS_BAR, 0, SBI_SAVELOAD_FINISH);
_sl.saveinprogress = false;
+
+#ifdef __EMSCRIPTEN__
+ EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
+#endif
}
/** Set the error message from outside of the actual loading/saving of the game (AfterLoadGame and friends) */
diff --git a/src/video/sdl2_v.cpp b/src/video/sdl2_v.cpp
index 09d0e8a0b..68b0aa983 100644
--- a/src/video/sdl2_v.cpp
+++ b/src/video/sdl2_v.cpp
@@ -27,6 +27,10 @@
#include <mutex>
#include <condition_variable>
#include <algorithm>
+#ifdef __EMSCRIPTEN__
+# include <emscripten.h>
+# include <emscripten/html5.h>
+#endif
#include "../safeguards.h"
@@ -673,7 +677,19 @@ void VideoDriver_SDL::LoopOnce()
InteractiveRandom(); // randomness
while (PollEvent() == -1) {}
- if (_exit_game) return;
+ if (_exit_game) {
+#ifdef __EMSCRIPTEN__
+ /* Emscripten is event-driven, and as such the main loop is inside
+ * the browser. So if _exit_game goes true, the main loop ends (the
+ * cancel call), but we still have to call the cleanup that is
+ * normally done at the end of the main loop for non-Emscripten.
+ * After that, Emscripten just halts, and the HTML shows a nice
+ * "bye, see you next time" message. */
+ emscripten_cancel_main_loop();
+ MainLoopCleanup();
+#endif
+ return;
+ }
mod = SDL_GetModState();
keys = SDL_GetKeyboardState(&numkeys);
@@ -722,9 +738,17 @@ void VideoDriver_SDL::LoopOnce()
_local_palette = _cur_palette;
} else {
/* Release the thread while sleeping */
- if (_draw_mutex != nullptr) draw_lock.unlock();
- CSleep(1);
- if (_draw_mutex != nullptr) draw_lock.lock();
+ if (_draw_mutex != nullptr) {
+ draw_lock.unlock();
+ CSleep(1);
+ draw_lock.lock();
+ } else {
+/* Emscripten is running an event-based mainloop; there is already some
+ * downtime between each iteration, so no need to sleep. */
+#ifndef __EMSCRIPTEN__
+ CSleep(1);
+#endif
+ }
NetworkDrawChatMessage();
DrawMouseCursor();
@@ -778,6 +802,10 @@ void VideoDriver_SDL::MainLoop()
DEBUG(driver, 1, "SDL2: using %sthreads", _draw_threaded ? "" : "no ");
+#ifdef __EMSCRIPTEN__
+ /* Run the main loop event-driven, based on RequestAnimationFrame. */
+ emscripten_set_main_loop_arg(&this->EmscriptenLoop, this, 0, 1);
+#else
while (!_exit_game) {
LoopOnce();
}
@@ -803,6 +831,15 @@ void VideoDriver_SDL::MainLoopCleanup()
_draw_mutex = nullptr;
_draw_signal = nullptr;
}
+
+#ifdef __EMSCRIPTEN__
+ emscripten_exit_pointerlock();
+ /* In effect, the game ends here. As emscripten_set_main_loop() caused
+ * the stack to be unwound, the code after MainLoop() in
+ * openttd_main() is never executed. */
+ EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
+ EM_ASM(if (window["openttd_exit"]) openttd_exit());
+#endif
}
bool VideoDriver_SDL::ChangeResolution(int w, int h)
diff --git a/src/video/sdl2_v.h b/src/video/sdl2_v.h
index 6318c403f..c2ac87a06 100644
--- a/src/video/sdl2_v.h
+++ b/src/video/sdl2_v.h
@@ -46,6 +46,11 @@ private:
void MainLoopCleanup();
bool CreateMainSurface(uint w, uint h, bool resize);
+#ifdef __EMSCRIPTEN__
+ /* Convert a constant pointer back to a non-constant pointer to a member function. */
+ static void EmscriptenLoop(void *self) { ((VideoDriver_SDL *)self)->LoopOnce(); }
+#endif
+
/**
* This is true to indicate that keyboard input is in text input mode, and SDL_TEXTINPUT events are enabled.
*/