From d15dc9f40f5a20bff452547a2dcb15231f9f969d Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Sat, 5 Dec 2020 21:57:47 +0100 Subject: [PATCH] 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 --- .github/workflows/ci-build.yml | 49 ++++++ CMakeLists.txt | 37 +++++ cmake/Options.cmake | 8 +- 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 +++++++++++++++++++++++++ src/fontcache.cpp | 3 +- src/gfx_type.h | 8 +- src/ini.cpp | 7 + src/network/core/address.cpp | 10 +- src/network/core/os_abstraction.h | 24 ++- src/network/network.cpp | 11 ++ src/network/network_content.cpp | 15 ++ src/network/network_gui.cpp | 17 ++ src/openttd.cpp | 14 ++ src/saveload/saveload.cpp | 7 + src/video/sdl2_v.cpp | 45 +++++- src/video/sdl2_v.h | 5 + 25 files changed, 844 insertions(+), 12 deletions(-) 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 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 0000000000000000000000000000000000000000..d4c4aaf75e34c7e22018990178313bc0fcf4a6fc GIT binary patch literal 4824 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 + + + + + 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. */