From d15dc9f40f5a20bff452547a2dcb15231f9f969d Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Sat, 5 Dec 2020 21:57:47 +0100 Subject: 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 --- os/emscripten/Dockerfile | 4 + os/emscripten/README.md | 40 +++++++ os/emscripten/cmake/FindLibLZMA.cmake | 20 ++++ os/emscripten/cmake/FindPNG.cmake | 7 ++ os/emscripten/cmake/FindSDL2.cmake | 7 ++ os/emscripten/cmake/FindZLIB.cmake | 7 ++ os/emscripten/emsdk-liblzma.patch | 213 ++++++++++++++++++++++++++++++++++ os/emscripten/loading.png | Bin 0 -> 4824 bytes os/emscripten/pre.js | 93 +++++++++++++++ os/emscripten/shell.html | 205 ++++++++++++++++++++++++++++++++ 10 files changed, 596 insertions(+) create mode 100644 os/emscripten/Dockerfile create mode 100644 os/emscripten/README.md create mode 100644 os/emscripten/cmake/FindLibLZMA.cmake create mode 100644 os/emscripten/cmake/FindPNG.cmake create mode 100644 os/emscripten/cmake/FindSDL2.cmake create mode 100644 os/emscripten/cmake/FindZLIB.cmake create mode 100644 os/emscripten/emsdk-liblzma.patch create mode 100755 os/emscripten/loading.png create mode 100644 os/emscripten/pre.js create mode 100644 os/emscripten/shell.html (limited to 'os') 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 + 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 +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 Binary files /dev/null and b/os/emscripten/loading.png 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 @@ + + + + + + OpenTTD + + + +
+
+
+ Loading ... +
+
+
+
+
+
+
+ Warning: savegames are stored in the Indexed DB of your browser.
Your browser can delete savegames without notice! +
+
+
+ +
+ + + {{{ SCRIPT }}} + + -- cgit v1.2.3-70-g09d2