1
0
mirror of https://github.com/OpenTTD/OpenTTD.git synced 2025-08-24 06:59:10 +00:00

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>
This commit is contained in:
Patric Stout
2020-12-05 21:57:47 +01:00
committed by Patric Stout
parent 2da07f7615
commit d15dc9f40f
25 changed files with 844 additions and 12 deletions

4
os/emscripten/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM emscripten/emsdk
COPY emsdk-liblzma.patch /
RUN cd /emsdk/upstream/emscripten && patch -p1 < /emsdk-liblzma.patch

40
os/emscripten/README.md Normal file
View File

@@ -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 .

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
+'''

BIN
os/emscripten/loading.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

93
os/emscripten/pre.js Normal file
View File

@@ -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) { });
}
});

205
os/emscripten/shell.html Normal file

File diff suppressed because one or more lines are too long