diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 31d0b511da..8794b79dae 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 15c6e03c0d..ca2284ffc1 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 977d54f061..c94a193b32 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 0000000000..1278a088ff --- /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 0000000000..4c5d7508cc --- /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 0000000000..99d1ca640a --- /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 0000000000..2616af33d9 --- /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 0000000000..54553958b6 --- /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 0000000000..2ade2ba1b0 --- /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 0000000000..103adae0cc --- /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 0000000000..d4c4aaf75e 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 0000000000..5cbd899e04 --- /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 0000000000..17ea5b414f --- /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 }}} + + diff --git a/src/fontcache.cpp b/src/fontcache.cpp index 051c4ee1db..79683d193c 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 6fca2228df..452bc2c7e8 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 6948bc1ea3..fc9b1e8fd2 100644 --- a/src/ini.cpp +++ b/src/ini.cpp @@ -13,6 +13,9 @@ #include "string_func.h" #include "fileio_func.h" #include +#ifdef __EMSCRIPTEN__ +# include +#endif #if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 500) # include @@ -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 c2fecc7ff2..1aaa0b5fba 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 01ab68b278..be8b8f919a 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 # include # include + +# 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 0e3d086301..a100b6b959 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 5e401d3e92..0140d3ef20 100644 --- a/src/network/network_content.cpp +++ b/src/network/network_content.cpp @@ -23,6 +23,10 @@ #include #endif +#ifdef __EMSCRIPTEN__ +# include +#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 c430c47e5e..47bf8fb69c 100644 --- a/src/network/network_gui.cpp +++ b/src/network/network_gui.cpp @@ -39,6 +39,9 @@ #include "../safeguards.h" +#ifdef __EMSCRIPTEN__ +# include +#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(WID_NG_NEWGRF_SEL)->SetDisplayedPlane(sel == nullptr || !sel->online || sel->info.grfconfig == nullptr); this->GetWidget(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 4aeed39282..9cc5d2f205 100644 --- a/src/openttd.cpp +++ b/src/openttd.cpp @@ -73,6 +73,11 @@ #include "safeguards.h" +#ifdef __EMSCRIPTEN__ +# include +# include +#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 999a381d71..5288f9047d 100644 --- a/src/saveload/saveload.cpp +++ b/src/saveload/saveload.cpp @@ -45,6 +45,9 @@ #include "../error.h" #include #include +#ifdef __EMSCRIPTEN__ +# include +#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 09d0e8a0bc..68b0aa983d 100644 --- a/src/video/sdl2_v.cpp +++ b/src/video/sdl2_v.cpp @@ -27,6 +27,10 @@ #include #include #include +#ifdef __EMSCRIPTEN__ +# include +# include +#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 6318c403f7..c2ac87a062 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. */