From 63b0106de896721b7da4864cd0efc81cabbc7be8 Mon Sep 17 00:00:00 2001 From: Ethan O'Brien Date: Tue, 31 Dec 2024 09:31:33 -0600 Subject: [PATCH] Fix emscripten support --- .gitignore | 14 + cmake/FindSpeexDSP.cmake | 40 +++ data/language/en-GB.txt | 2 + emscripten/Dockerfile | 27 ++ emscripten/build_emscripten.sh | 162 +++++++++++ emscripten/static/index.html | 30 ++ emscripten/static/index.js | 271 ++++++++++++++++++ src/openrct2-cli/CMakeLists.txt | 3 + src/openrct2-ui/CMakeLists.txt | 22 +- src/openrct2-ui/TextComposition.cpp | 18 ++ src/openrct2-ui/Ui.cpp | 10 + src/openrct2-ui/UiContext.Linux.cpp | 8 + src/openrct2-ui/UiContext.cpp | 33 +++ src/openrct2-ui/UiStringIds.h | 6 +- .../drawing/engines/opengl/OpenGLAPIProc.h | 17 ++ .../engines/opengl/OpenGLDrawingEngine.cpp | 2 + src/openrct2-ui/input/MouseInput.cpp | 4 + src/openrct2-ui/windows/About.cpp | 19 ++ src/openrct2-ui/windows/Options.cpp | 20 ++ src/openrct2/CMakeLists.txt | 20 +- src/openrct2/Context.cpp | 25 +- src/openrct2/Version.h | 6 +- src/openrct2/core/Imaging.cpp | 25 ++ src/openrct2/core/Money.hpp | 2 +- src/openrct2/core/Speed.hpp | 2 +- src/openrct2/platform/Platform.Linux.cpp | 2 +- src/openrct2/platform/Platform.Posix.cpp | 2 +- src/openrct2/ride/VehicleRiderControl.cpp | 2 +- 28 files changed, 769 insertions(+), 25 deletions(-) create mode 100644 cmake/FindSpeexDSP.cmake create mode 100644 emscripten/Dockerfile create mode 100755 emscripten/build_emscripten.sh create mode 100644 emscripten/static/index.html create mode 100644 emscripten/static/index.js diff --git a/.gitignore b/.gitignore index af2ff4ec57..9b7bcddbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -310,3 +310,17 @@ discord-rpc # CMake cmake-build-*/ + +# Emscripten +src/thirdparty/nlohmann/ +emscripten/ext/ +emscripten/www/ +emscripten/temp/ +emscripten/Makefile +emscripten/CMake* +emscripten/cmake* +emscripten/*.a +emscripten/*.js +emscripten/*.wasm +emscripten/static/assets* +emscripten-output/ diff --git a/cmake/FindSpeexDSP.cmake b/cmake/FindSpeexDSP.cmake new file mode 100644 index 0000000000..d1267499fd --- /dev/null +++ b/cmake/FindSpeexDSP.cmake @@ -0,0 +1,40 @@ +# +# - Find speexdsp libraries +# +# SPEEXDSP_INCLUDE_DIRS - where to find speexdsp headers. +# SPEEXDSP_LIBRARIES - List of libraries when using speexdsp. +# SPEEXDSP_FOUND - True if speexdsp is found. + +find_package(PkgConfig QUIET) +pkg_search_module(PC_SPEEXDSP QUIET speexdsp) + +find_path(SPEEXDSP_INCLUDE_DIR + NAMES + speex/speex_resampler.h + HINTS + ${PC_SPEEXDSP_INCLUDE_DIRS} +) + +find_library(SPEEXDSP_LIBRARY + NAMES + speexdsp + HINTS + ${PC_SPEEXDSP_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SpeexDSP + REQUIRED_VARS SPEEXDSP_LIBRARY SPEEXDSP_INCLUDE_DIR + VERSION_VAR PC_SPEEXDSP_VERSION) + +if(SPEEXDSP_FOUND) + set(SPEEXDSP_LIBRARIES ${SPEEXDSP_LIBRARY}) + set(SPEEXDSP_INCLUDE_DIRS ${SPEEXDSP_INCLUDE_DIR}) + set(SPEEX_INCLUDE_DIRS ${SPEEXDSP_INCLUDE_DIR}) +else() + set(SPEEXDSP_LIBRARIES) + set(SPEEXDSP_INCLUDE_DIRS) + set(SPEEX_INCLUDE_DIRS) +endif() + +mark_as_advanced(SPEEXDSP_LIBRARIES SPEEXDSP_INCLUDE_DIRS SPEEX_INCLUDE_DIRS) diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index c757fb185f..c5acd146ab 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3787,3 +3787,5 @@ STR_6709 :Enter Smooth Strength between {COMMA16} and {COMMA16} STR_6710 :Stable sort STR_6711 :Filename: STR_6712 :Save +STR_6713 :Export emscripten data +STR_6714 :Import emscripten data diff --git a/emscripten/Dockerfile b/emscripten/Dockerfile new file mode 100644 index 0000000000..a9d493de37 --- /dev/null +++ b/emscripten/Dockerfile @@ -0,0 +1,27 @@ +FROM docker.io/library/fedora:41 AS builder + +RUN dnf update -y && dnf install -y git cmake make gcc g++ nlohmann-json-devel autoreconf libtool openssl-devel libcurl-devel fontconfig-devel libzip-devel SDL2-devel flac-devel libvorbis-devel zip speexdsp-devel + +WORKDIR / + +RUN git clone https://github.com/emscripten-core/emsdk.git + +WORKDIR /emsdk/ + +# Pin version - to prevent sudden breakage of the CI +RUN ./emsdk install 3.1.74 +RUN ./emsdk activate 3.1.74 + +WORKDIR /openrct2/ + +COPY ./ ./ + +RUN rm -rf emscripten/temp/ emscripten/www/ emscripten/ext/ + +WORKDIR /emsdk/ + +RUN . ./emsdk_env.sh && cd /openrct2/emscripten/ && ./build_emscripten.sh + +FROM scratch AS export + +COPY --from=builder /openrct2/emscripten/www/* . diff --git a/emscripten/build_emscripten.sh b/emscripten/build_emscripten.sh new file mode 100755 index 0000000000..c484efd4ef --- /dev/null +++ b/emscripten/build_emscripten.sh @@ -0,0 +1,162 @@ + +cd "$(dirname "$0")" + +START_DIR=$(pwd) +ICU_ROOT=$(pwd)/ext/icu/icu4c/source +JSON_DIR=/usr/include/nlohmann/ + +build_ext() { + mkdir -p ext/ + cd ext/ + # Pin versions - to prevent sudden breakage + if [ ! -d "speexdsp" ]; then + git clone https://gitlab.xiph.org/xiph/speexdsp.git --depth 1 + cd speexdsp + git fetch --depth=1 origin dbd421d149a9c362ea16150694b75b63d757a521 + git checkout dbd421d149a9c362ea16150694b75b63d757a521 + cd .. + fi + if [ ! -d "icu" ]; then + git clone https://github.com/unicode-org/icu.git --depth 1 + cd icu + git fetch --depth=1 origin ba012a74a11405a502b6890e710bfb58cef7a2c7 + git checkout ba012a74a11405a502b6890e710bfb58cef7a2c7 + cd .. + fi + if [ ! -d "libzip" ]; then + git clone https://github.com/nih-at/libzip.git --depth 1 + cd libzip + git fetch --depth=1 origin 8352d224d458d86949fd9148dd33332f50a25c7f + git checkout 8352d224d458d86949fd9148dd33332f50a25c7f + cd .. + fi + if [ ! -d "zlib" ]; then + git clone https://github.com/madler/zlib.git --depth 1 + cd zlib + git fetch --depth=1 origin ef24c4c7502169f016dcd2a26923dbaf3216748c + git checkout ef24c4c7502169f016dcd2a26923dbaf3216748c + cd .. + fi + if [ ! -d "vorbis" ]; then + git clone https://gitlab.xiph.org/xiph/vorbis.git --depth 1 + cd vorbis + git fetch --depth=1 origin bb4047de4c05712bf1fd49b9584c360b8e4e0adf + git checkout bb4047de4c05712bf1fd49b9584c360b8e4e0adf + cd .. + fi + if [ ! -d "ogg" ]; then + git clone https://gitlab.xiph.org/xiph/ogg.git --depth 1 + cd ogg + git fetch --depth=1 origin 7cf42ea17aef7bc1b7b21af70724840a96c2e7d0 + git checkout 7cf42ea17aef7bc1b7b21af70724840a96c2e7d0 + cd .. + fi + if [ ! -d "$JSON_DIR" ]; then + echo "$JSON_DIR does not exist. Set in build_emscripten.sh or install the nlohmann-json headers!" + exit 1 + fi + rm -rf ../../src/thirdparty/nlohmann + cp -r $JSON_DIR ../../src/thirdparty/nlohmann + + cd speexdsp + emmake ./autogen.sh + emmake ./configure --enable-shared --disable-neon + emmake make -j$(nproc) + cd $START_DIR/ext/ + + cd icu/icu4c/source + ac_cv_namespace_ok=yes icu_cv_host_frag=mh-linux emmake ./configure \ + --enable-release \ + --enable-shared \ + --disable-icu-config \ + --disable-extras \ + --disable-icuio \ + --disable-layoutex \ + --disable-tools \ + --disable-tests \ + --disable-samples + emmake make -j$(nproc) + cd $START_DIR/ext/ + + cd zlib + emcmake cmake ./ + emmake make zlib -j$(nproc) + emmake make install + ZLIB_ROOT=$(pwd) + cd $START_DIR/ext/ + + cd libzip + mkdir -p build/ + cd build/ + emcmake cmake ../ -DZLIB_INCLUDE_DIR="$ZLIB_ROOT" -DZLIB_LIBRARY="$ZLIB_ROOT/libz.a" + emmake make zip -j$(nproc) + emmake make install + cd $START_DIR/ext/ + + cd ogg + mkdir -p build/ + cd build/ + emcmake cmake ../ + emmake make -j$(nproc) + emmake make install + cd $START_DIR/ext/ + + cd vorbis + mkdir -p build/ + cd build/ + emcmake cmake ../ + emmake make -j$(nproc) + emmake make install + + cd $START_DIR +} + +build_assets() { + mkdir temp/ + cd temp/ + cmake ../../ -DMACOS_BUNDLE=off -DDISABLE_NETWORK=on -DDISABLE_GUI=off + make openrct2-cli -j$(nproc) + make g2 -j$(nproc) + DESTDIR=. make install + mkdir -p ../static/assets/ + cp -r usr/local/share/openrct2/* ../static/assets/ + cd ../static/assets + zip -r ../assets.zip * + cd ../ + rm -rf assets/ + + cd $START_DIR +} + +if [ "$1" != "skip" ] ; then +build_ext +build_assets +fi + +emcmake cmake ../ \ + -DDISABLE_NETWORK=ON \ + -DDISABLE_HTTP=ON \ + -DDISABLE_TTF=ON \ + -DDISABLE_FLAC=ON \ + -DDISABLE_DISCORD_RPC=ON \ + -DCMAKE_SYSTEM_NAME=Emscripten \ + -DCMAKE_BUILD_TYPE=Release \ + -DSPEEXDSP_INCLUDE_DIR="$(pwd)/ext/speexdsp/include/" \ + -DSPEEXDSP_LIBRARY="$(pwd)/ext/speexdsp/libspeexdsp/.libs/libspeexdsp.a" \ + -DICU_INCLUDE_DIR="$ICU_ROOT/common" \ + -DICU_DATA_LIBRARIES=$ICU_ROOT/lib/libicuuc.so \ + -DICU_DT_LIBRARY_RELEASE="$ICU_ROOT/stubdata/libicudata.so" \ + -DLIBZIP_LIBRARIES="$(pwd)/ext/libzip/build/lib/libzip.a" \ + -DEMSCRIPTEN_FLAGS="-s USE_SDL=2 -s USE_BZIP2=1 -s USE_LIBPNG=1 -pthread -O3" \ + -DEMSCRIPTEN_LDFLAGS="-Wno-pthreads-mem-growth -s ASYNCIFY -s FULL_ES3 -s SAFE_HEAP=0 -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=4GB -s INITIAL_MEMORY=2GB -s MAX_WEBGL_VERSION=2 -s PTHREAD_POOL_SIZE=120 -pthread -sEXPORTED_RUNTIME_METHODS=FS,callMain,UTF8ToString,stringToNewUTF8 -lidbfs.js --use-preload-plugins -s MODULARIZE=1 -s 'EXPORT_NAME=\"OPENRCT2_WEB\"'" + +emmake make -j$(nproc) + +rm -rf www/ +mkdir -p www/ +cd www/ +cp -r ../openrct2.* ./ +cp -r ../static/* ./ +cp -r ../static/.* ./ + +echo "finished!" diff --git a/emscripten/static/index.html b/emscripten/static/index.html new file mode 100644 index 0000000000..546766358c --- /dev/null +++ b/emscripten/static/index.html @@ -0,0 +1,30 @@ + + + + OpenRCT2 + + + + + + +

Please wait... Loading webassembly

+ + + + + diff --git a/emscripten/static/index.js b/emscripten/static/index.js new file mode 100644 index 0000000000..55fe0905ea --- /dev/null +++ b/emscripten/static/index.js @@ -0,0 +1,271 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +// assets_version should be updated when assets need to be re-downloaded on the client +const assets_version = "0.4.17-1"; +(async () => +{ + await new Promise(res => window.addEventListener("DOMContentLoaded", res)); + if (!window.SharedArrayBuffer) + { + document.getElementById("loadingWebassembly").innerText = "Error! SharedArrayBuffer is not defined. This page required the CORP and COEP response headers."; + } + + window.Module = await window.OPENRCT2_WEB( + { + noInitialRun: true, + arguments: [], + preRun: [], + postRun: [], + canvas: document.getElementById("canvas"), + print: function(msg) + { + console.log(msg); + }, + printErr: function(msg) + { + console.log(msg); + }, + totalDependencies: 0, + monitorRunDependencies: () => {}, + locateFile: function(fileName) + { + console.log("loading", fileName); + return fileName; + }, + funcs: { + export: () => + { + const zip = zipFolder("/persistant/"); + zip.generateAsync({type: "blob"}).then(blob => { + const a = document.createElement("a"); + + a.href = URL.createObjectURL(blob); + a.download = "OpenRCT2-emscripten.zip"; + a.click(); + setTimeout(() => URL.revokeObjectURL(a.href), 1000); + }) + }, + import: () => + { + if (!confirm("Are you sure? This will wipe all current data.")) return; + alert("Select a zip file"); + const input = document.createElement("input"); + input.type = "file"; + input.addEventListener("change", async (e) => { + let zip = new JSZip(); + try { + zip = await zip.loadAsync(e.target.files[0]); + } catch(e) { + alert("Not a zip file!"); + return; + } + await clearDatabase("/persistant/"); + for (const k in zip.files) { + const entry = zip.files[k]; + if (entry.dir) { + try { + Module.FS.mkdir("/"+k); + } catch(e) {} + } else { + Module.FS.writeFile("/"+k, await entry.async("uint8array")); + } + } + console.log("Database restored"); + }) + input.click(); + } + } + }); + + Module.FS.mkdir("/persistant"); + Module.FS.mount(Module.FS.filesystems.IDBFS, {autoPersist: true}, '/persistant'); + + Module.FS.mkdir("/RCT"); + Module.FS.mount(Module.FS.filesystems.IDBFS, {autoPersist: true}, '/RCT'); + + Module.FS.mkdir("/OpenRCT2"); + Module.FS.mount(Module.FS.filesystems.IDBFS, {autoPersist: true}, '/OpenRCT2'); + + await new Promise(res => Module.FS.syncfs(true, res)); + + let configExists = fileExists("/persistant/config.ini"); + if (!configExists) + { + Module.FS.writeFile("/persistant/config.ini", ` +[general] +game_path = "/RCT" +uncap_fps = true +window_scale = 1.750000 +`); + } + + await updateAssets(); + + Module.FS.writeFile("/OpenRCT2/changelog.txt", `EMSCRIPTEN --- README + +Since we're running in the web browser, we don't have direct access to the file system. +All save data is saved under the directory /persistant. + +ALWAYS be sure to save to /persistant/saves when saving a game! Otherwise it will be wiped! + +You can import/export the /persistant folder in the options menu.`); + document.getElementById("loadingWebassembly").remove(); + + let filesFound = fileExists("/RCT/Data/ch.dat"); + + if (!filesFound) + { + document.getElementById("beforeLoad").style.display = ""; + await new Promise(res => + { + document.getElementById("selectFile").addEventListener("change", async (e) => + { + if (await extractZip(e.target.files[0], (zip) => + { + if (zip !== null) + { + if (zip.file("Data/ch.dat")) + { + document.getElementById("beforeLoad").remove(); + return "/RCT/"; + } + else if (zip.file("RCT/Data/ch.dat")) + { + document.getElementById("beforeLoad").remove(); + return "/"; + } + } + document.getElementById("statusMsg").innerText = "That doesn't look right. Your file should be a zip file containing Data/ch.dat. Please select your OpenRCT2 contents (zip file):"; + return false; + })) + { + res(); + } + }); + }); + } + Module.canvas.style.display = ""; + Module.callMain(["--user-data-path=/persistant/", "--openrct2-data-path=/OpenRCT2/"]); +})(); + +async function updateAssets() { + let currentVersion = ""; + try { + currentVersion = Module.FS.readFile("/OpenRCT2/version", {encoding: "utf8"}); + } catch(e) {}; + console.log("Found asset version", currentVersion); + + if (currentVersion !== assets_version || assets_version === "DEV") + { + console.log("Updating assets to", assets_version); + document.getElementById("loadingWebassembly").innerText = "Asset update found. Downloading..."; + await clearDatabase("/OpenRCT2/"); + await extractZip(await (await fetch("assets.zip")).blob(), () => + { + return "/OpenRCT2/"; + }); + Module.FS.writeFile("/OpenRCT2/version", assets_version.toString()); + } +} + +async function extractZip(data, checkZip) { + let zip = new JSZip(); + let contents; + try { + contents = await zip.loadAsync(data); + } catch(e) { + if (typeof checkZip === "function") + { + checkZip(null); + } + throw e; + } + let base = "/"; + if (typeof checkZip === "function") + { + const cont = checkZip(contents); + if (cont === false) return false; + base = cont; + } + for (const k in contents.files) { + const entry = contents.files[k]; + if (entry.dir) + { + try { + Module.FS.mkdir(base+k); + } catch(e) {} + } + else + { + Module.FS.writeFile(base+k, await entry.async("uint8array")); + } + } + return true; +} +async function clearDatabase(dir) { + await new Promise(res => Module.FS.syncfs(false, res)); + const processFolder = (path) => { + let contents; + try { + contents = Module.FS.readdir(path); + } catch(e) { + return; + } + contents.forEach((entry) => { + if ([".", ".."].includes(entry)) return; + try { + Module.FS.readFile(path + entry); + Module.FS.unlink(path + entry); + } catch(e) { + processFolder(path + entry + "/"); + } + }) + if (path === dir) return; + try { + Module.FS.rmdir(path, {recursive: true}); + } catch(e) { + console.log("Could not remove:", path); + } + } + processFolder(dir); + await new Promise(res => Module.FS.syncfs(false, res)); +} +function zipFolder(folder) { + let zip = new JSZip(); + const processFolder = (name) => { + let contents; + try { + contents = Module.FS.readdir(name); + } catch(e) { + return; + } + contents.forEach((entry) => { + if ([".", ".."].includes(entry)) return; + try { + Module.FS.readFile(name + entry); + processFile(name + entry); + } catch(e) { + processFolder(name + entry + "/"); + } + }) + } + const processFile = (name) => { + zip.file(name, Module.FS.readFile(name)); + } + processFolder(folder); + return zip; +} +function fileExists(path) { + try { + Module.FS.readFile(path); + return true; + } catch(e) {}; + return false; +} diff --git a/src/openrct2-cli/CMakeLists.txt b/src/openrct2-cli/CMakeLists.txt index 0c0d258704..6641903752 100644 --- a/src/openrct2-cli/CMakeLists.txt +++ b/src/openrct2-cli/CMakeLists.txt @@ -17,5 +17,8 @@ file(GLOB_RECURSE OPENRCT2_CLI_SOURCES add_executable(${PROJECT_NAME} ${OPENRCT2_CLI_SOURCES}) target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/..") ipo_set_target_properties(${PROJECT_NAME}) +if (CMAKE_SYSTEM_NAME MATCHES "Emscripten") + target_link_libraries(${PROJECT_NAME} ${ICU_DT_LIBRARY_RELEASE} ${ICU_DATA_LIBRARIES}) +endif () target_link_libraries(${PROJECT_NAME} libopenrct2 Threads::Threads) target_link_platform_libraries(${PROJECT_NAME}) diff --git a/src/openrct2-ui/CMakeLists.txt b/src/openrct2-ui/CMakeLists.txt index d98a5a159f..e08dc85a44 100644 --- a/src/openrct2-ui/CMakeLists.txt +++ b/src/openrct2-ui/CMakeLists.txt @@ -11,7 +11,18 @@ option(DISABLE_VORBIS "Disable OGG/VORBIS support.") option(DISABLE_OPENGL "Disable OpenGL support.") # Third party libraries -if (MSVC) +if (CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(USE_FLAGS "${EMSCRIPTEN_FLAGS}") + set(SHARED_FLAGS "-fexceptions") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS}") + find_package(SpeexDSP REQUIRED) + if (NOT DISABLE_VORBIS) + PKG_CHECK_MODULES(OGG REQUIRED IMPORTED_TARGET ogg) + PKG_CHECK_MODULES(VORBISFILE REQUIRED IMPORTED_TARGET vorbisfile vorbisenc vorbis) + endif () +elseif (MSVC) find_package(SDL2 REQUIRED) find_library(SPEEX_LDFLAGS libspeexdsp) if (NOT DISABLE_FLAC) @@ -33,7 +44,7 @@ else () endif () endif () -if (NOT DISABLE_OPENGL) +if (NOT DISABLE_OPENGL AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") # GL doesn't work nicely with macOS, while find_package doesn't work with multiarch on Ubuntu. if (APPLE) find_package(OpenGL REQUIRED) @@ -61,7 +72,12 @@ SET_CHECK_CXX_FLAGS(${PROJECT_NAME}) ipo_set_target_properties(${PROJECT_NAME}) # mingw builds cannot use the PkgConfig imported targets -if (NOT MSVC AND NOT WIN32) +if (CMAKE_SYSTEM_NAME MATCHES "Emscripten") + target_link_libraries(${PROJECT_NAME} "libopenrct2" + ${SPEEXDSP_LIBRARIES} + ${ICU_DATA_LIBRARIES} + ${ICU_DT_LIBRARY_RELEASE}) +elseif (NOT MSVC AND NOT WIN32) target_link_libraries(${PROJECT_NAME} "libopenrct2" PkgConfig::SDL2 PkgConfig::SPEEX) diff --git a/src/openrct2-ui/TextComposition.cpp b/src/openrct2-ui/TextComposition.cpp index c200148427..ac659aa9df 100644 --- a/src/openrct2-ui/TextComposition.cpp +++ b/src/openrct2-ui/TextComposition.cpp @@ -19,6 +19,10 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include +#endif + #ifdef __MACOSX__ // macOS uses COMMAND rather than CTRL for many keyboard shortcuts #define KEYBOARD_PRIMARY_MODIFIER KMOD_GUI @@ -170,7 +174,21 @@ void TextComposition::HandleMessage(const SDL_Event* e) case SDLK_c: if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && _session.Length) { +#ifndef __EMSCRIPTEN__ SDL_SetClipboardText(_session.Buffer->c_str()); +#else + MAIN_THREAD_EM_ASM( + { + try + { + navigator.clipboard.writeText(UTF8ToString($0)); + } + catch (e) + { + }; + }, + _session.Buffer->c_str()); +#endif ContextShowError(STR_COPY_INPUT_TO_CLIPBOARD, STR_NONE, {}); } break; diff --git a/src/openrct2-ui/Ui.cpp b/src/openrct2-ui/Ui.cpp index f4f5c1f328..3e32dd7270 100644 --- a/src/openrct2-ui/Ui.cpp +++ b/src/openrct2-ui/Ui.cpp @@ -24,6 +24,10 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include +#endif + using namespace OpenRCT2; using namespace OpenRCT2::Audio; using namespace OpenRCT2::Ui; @@ -43,6 +47,12 @@ int NormalisedMain(int argc, const char** argv) int main(int argc, const char** argv) #endif { +#ifdef __EMSCRIPTEN__ + MAIN_THREAD_EM_ASM({ + specialHTMLTargets["!canvas"] = Module.canvas; + Module.canvas.addEventListener("contextmenu", (e) = > { e.preventDefault(); }); + }); +#endif std::unique_ptr context; int32_t rc = EXIT_SUCCESS; int runGame = CommandLineRun(argv, argc); diff --git a/src/openrct2-ui/UiContext.Linux.cpp b/src/openrct2-ui/UiContext.Linux.cpp index 8e5b18c4e4..595311a107 100644 --- a/src/openrct2-ui/UiContext.Linux.cpp +++ b/src/openrct2-ui/UiContext.Linux.cpp @@ -27,6 +27,10 @@ #include #include + #ifdef __EMSCRIPTEN__ + #include + #endif + namespace OpenRCT2::Ui { enum class DIALOG_TYPE @@ -129,8 +133,12 @@ namespace OpenRCT2::Ui void OpenURL(const std::string& url) override { + #ifndef __EMSCRIPTEN__ std::string cmd = String::stdFormat("xdg-open %s", url.c_str()); Platform::Execute(cmd); + #else + MAIN_THREAD_EM_ASM({ window.open(UTF8ToString($0)); }, url.c_str()); + #endif } std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override diff --git a/src/openrct2-ui/UiContext.cpp b/src/openrct2-ui/UiContext.cpp index cb8d98b072..261350afc7 100644 --- a/src/openrct2-ui/UiContext.cpp +++ b/src/openrct2-ui/UiContext.cpp @@ -49,6 +49,11 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include + #include +#endif + using namespace OpenRCT2; using namespace OpenRCT2::Drawing; using namespace OpenRCT2::Scripting; @@ -713,7 +718,25 @@ public: bool SetClipboardText(const utf8* target) override { +#ifndef __EMSCRIPTEN__ return (SDL_SetClipboardText(target) == 0); +#else + return ( + MAIN_THREAD_EM_ASM_INT( + { + try + { + navigator.clipboard.writeText(UTF8ToString($0)); + return 0; + } + catch (e) + { + return -1; + }; + }, + gVersionInfoFull) + == 0); +#endif } ITitleSequencePlayer* GetTitleSequencePlayer() override @@ -753,9 +776,19 @@ private: void CreateWindow(const ScreenCoordsXY& windowPos) { +#ifdef __EMSCRIPTEN__ + MAIN_THREAD_EM_ASM({ + Module.canvas.width = window.innerWidth; + Module.canvas.height = window.innerHeight; + }); + int32_t width = 0; + int32_t height = 0; + emscripten_get_canvas_element_size("!canvas", &width, &height); +#else // Get saved window size int32_t width = Config::Get().general.WindowWidth; int32_t height = Config::Get().general.WindowHeight; +#endif if (width <= 0) width = 640; if (height <= 0) diff --git a/src/openrct2-ui/UiStringIds.h b/src/openrct2-ui/UiStringIds.h index 5b8eeffe9f..596bfa5a9d 100644 --- a/src/openrct2-ui/UiStringIds.h +++ b/src/openrct2-ui/UiStringIds.h @@ -1115,6 +1115,10 @@ namespace OpenRCT2 STR_DRAWING_ENGINE_TIP = 5876, STR_EARLY_COMPLETION_TIP = 6227, STR_EDIT_ASSET_PACKS_BUTTON = 6640, +#ifdef __EMSCRIPTEN__ + STR_EXPORT_EMSCRIPTEN = 6713, + STR_IMPORT_EMSCRIPTEN = 6714, +#endif STR_EDIT_THEMES_BUTTON = 5153, STR_EDIT_THEMES_BUTTON_TIP = 5837, STR_EFFECTS_GROUP = 6256, @@ -2282,4 +2286,4 @@ namespace OpenRCT2 STR_ADJUST_SMALLER_WATER_TIP = 2380, STR_WATER = 2383, }; -} +} // namespace OpenRCT2 diff --git a/src/openrct2-ui/drawing/engines/opengl/OpenGLAPIProc.h b/src/openrct2-ui/drawing/engines/opengl/OpenGLAPIProc.h index 0245978654..b912855a5f 100644 --- a/src/openrct2-ui/drawing/engines/opengl/OpenGLAPIProc.h +++ b/src/openrct2-ui/drawing/engines/opengl/OpenGLAPIProc.h @@ -42,7 +42,11 @@ OPENGL_PROC(PFNGLATTACHSHADERPROC, glAttachShader) OPENGL_PROC(PFNGLBINDBUFFERPROC, glBindBuffer) OPENGL_PROC(PFNGLBINDFRAGDATALOCATIONPROC, glBindFragDataLocation) OPENGL_PROC(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer) +#ifndef FAKE__EMSCRIPTEN__ OPENGL_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray) +#else +extern "C" void glBindVertexArray(GLuint array); +#endif OPENGL_PROC(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer) OPENGL_PROC(PFNGLBUFFERDATAPROC, glBufferData) OPENGL_PROC(PFNGLBUFFERSUBDATAPROC, glBufferSubData) @@ -55,7 +59,11 @@ OPENGL_PROC(PFNGLDELETEBUFFERSPROC, glDeleteBuffers) OPENGL_PROC(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers) OPENGL_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram) OPENGL_PROC(PFNGLDELETESHADERPROC, glDeleteShader) +#ifndef FAKE__EMSCRIPTEN__ OPENGL_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays) +#else +extern "C" void glDeleteVertexArrays(GLsizei n, const GLuint* arrays); +#endif OPENGL_PROC(PFNGLDETACHSHADERPROC, glDetachShader) OPENGL_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray) OPENGL_PROC(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D) @@ -67,7 +75,11 @@ OPENGL_PROC(PFNGLGETPROGRAMIVPROC, glGetProgramiv) OPENGL_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog) OPENGL_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv) OPENGL_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation) +#ifndef FAKE__EMSCRIPTEN__ OPENGL_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays) +#else +extern "C" void glGenVertexArrays(GLsizei n, GLuint* arrays); +#endif OPENGL_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram) OPENGL_PROC(PFNGLSHADERSOURCEPROC, glShaderSource) OPENGL_PROC(PFNGLUNIFORM1IPROC, glUniform1i) @@ -82,6 +94,11 @@ OPENGL_PROC(PFNGLUNIFORM4FVPROC, glUniform4fv) OPENGL_PROC(PFNGLUSEPROGRAMPROC, glUseProgram) OPENGL_PROC(PFNGLVERTEXATTRIBIPOINTERPROC, glVertexAttribIPointer) OPENGL_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer) +#ifndef FAKE__EMSCRIPTEN__ OPENGL_PROC(PFNGLDRAWARRAYSINSTANCEDPROC, glDrawArraysInstanced) OPENGL_PROC(PFNGLVERTEXATTRIBDIVISORPROC, glVertexAttribDivisor) +#else +extern "C" void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount); +extern "C" void glVertexAttribDivisor(GLuint index, GLuint divisor); +#endif OPENGL_PROC(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate) diff --git a/src/openrct2-ui/drawing/engines/opengl/OpenGLDrawingEngine.cpp b/src/openrct2-ui/drawing/engines/opengl/OpenGLDrawingEngine.cpp index 333d0f5d95..b1ef7ea3bd 100644 --- a/src/openrct2-ui/drawing/engines/opengl/OpenGLDrawingEngine.cpp +++ b/src/openrct2-ui/drawing/engines/opengl/OpenGLDrawingEngine.cpp @@ -70,7 +70,9 @@ private: int32_t _drawCount = 0; + #ifndef NO_TTF uint32_t _ttfGlId = 0; + #endif struct { diff --git a/src/openrct2-ui/input/MouseInput.cpp b/src/openrct2-ui/input/MouseInput.cpp index fbe7cfca02..f4e4235bd4 100644 --- a/src/openrct2-ui/input/MouseInput.cpp +++ b/src/openrct2-ui/input/MouseInput.cpp @@ -605,6 +605,7 @@ static void InputViewportDragContinue() } } +#ifndef __EMSCRIPTEN__ const CursorState* cursorState = ContextGetCursorState(); if (cursorState->touch || Config::Get().general.InvertViewportDrag) { @@ -614,6 +615,9 @@ static void InputViewportDragContinue() { ContextSetCursorPosition(gInputDragLast); } +#else + gInputDragLast = newDragCoords; +#endif } static void InputViewportDragEnd() diff --git a/src/openrct2-ui/windows/About.cpp b/src/openrct2-ui/windows/About.cpp index 1ff71fe021..a06b76e633 100644 --- a/src/openrct2-ui/windows/About.cpp +++ b/src/openrct2-ui/windows/About.cpp @@ -21,6 +21,10 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include +#endif + namespace OpenRCT2::Ui::Windows { static constexpr int32_t WW = 400; @@ -119,7 +123,22 @@ namespace OpenRCT2::Ui::Windows ContextOpenWindowView(WV_NEW_VERSION_INFO); break; case WIDX_COPY_BUILD_INFO: +#ifndef __EMSCRIPTEN__ SDL_SetClipboardText(gVersionInfoFull); +#else + MAIN_THREAD_EM_ASM( + { + try + { + navigator.clipboard.writeText(UTF8ToString($0)); + } + catch (e) + { + // Ignore + }; + }, + gVersionInfoFull); +#endif break; case WIDX_CONTRIBUTORS_BUTTON: ContextOpenWindowView(WV_CONTRIBUTORS); diff --git a/src/openrct2-ui/windows/Options.cpp b/src/openrct2-ui/windows/Options.cpp index 7c8830003a..d21ece046d 100644 --- a/src/openrct2-ui/windows/Options.cpp +++ b/src/openrct2-ui/windows/Options.cpp @@ -46,6 +46,10 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include +#endif + using namespace OpenRCT2; using namespace OpenRCT2::Audio; @@ -218,6 +222,10 @@ namespace OpenRCT2::Ui::Windows WIDX_PATH_TO_RCT1_BUTTON, WIDX_PATH_TO_RCT1_CLEAR, WIDX_ASSET_PACKS, +#ifdef __EMSCRIPTEN__ + WIDX_EXPORT_EMSCRIPTEN_DATA, + WIDX_IMPORT_EMSCRIPTEN_DATA, +#endif }; // clang-format off @@ -404,6 +412,10 @@ namespace OpenRCT2::Ui::Windows MakeWidget ({ 24, 160}, {266, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_NONE, STR_STRING_TOOLTIP ), // RCT 1 path button MakeWidget ({289, 160}, { 11, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_CLOSE_X, STR_PATH_TO_RCT1_CLEAR_TIP ), // RCT 1 path clear button MakeWidget ({150, 176}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EDIT_ASSET_PACKS_BUTTON, STR_NONE ), // Asset packs +#ifdef __EMSCRIPTEN__ + MakeWidget ({150, 192}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EXPORT_EMSCRIPTEN, STR_NONE ), // Emscripten data export + MakeWidget ({150, 208}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_IMPORT_EMSCRIPTEN, STR_NONE ), // Emscripten data import +#endif kWidgetsEnd, }; @@ -1973,6 +1985,14 @@ namespace OpenRCT2::Ui::Windows case WIDX_ASSET_PACKS: ContextOpenWindow(WindowClass::AssetPacks); break; +#ifdef __EMSCRIPTEN__ + case WIDX_EXPORT_EMSCRIPTEN_DATA: + MAIN_THREAD_EM_ASM({ Module.funcs.export(); }); + break; + case WIDX_IMPORT_EMSCRIPTEN_DATA: + MAIN_THREAD_EM_ASM({ Module.funcs.import(); }); + break; +#endif } } diff --git a/src/openrct2/CMakeLists.txt b/src/openrct2/CMakeLists.txt index 89dc426696..c01de8e782 100644 --- a/src/openrct2/CMakeLists.txt +++ b/src/openrct2/CMakeLists.txt @@ -114,7 +114,19 @@ if (NOT DISABLE_GOOGLE_BENCHMARK) endif () # Third party libraries -if (MSVC) +if (CMAKE_SYSTEM_NAME MATCHES "Emscripten") + target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${ICU_INCLUDE_DIR}) + set(USE_FLAGS "${EMSCRIPTEN_FLAGS}") + set(SHARED_FLAGS "-fexceptions") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS}") + find_package(SpeexDSP REQUIRED) + if (NOT DISABLE_VORBIS) + PKG_CHECK_MODULES(OGG REQUIRED IMPORTED_TARGET ogg) + PKG_CHECK_MODULES(VORBISFILE REQUIRED IMPORTED_TARGET vorbisfile vorbisenc vorbis) + endif () +elseif (MSVC) find_package(png 1.6 REQUIRED) find_package(zlib REQUIRED) @@ -142,7 +154,7 @@ if (STATIC) ${ZLIB_STATIC_LIBRARIES} ${LIBZIP_STATIC_LIBRARIES}) else () - if (NOT MSVC) + if (NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") target_link_libraries(${PROJECT_NAME} PkgConfig::PNG PkgConfig::ZLIB @@ -171,7 +183,7 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(${PROJECT_NAME} Threads::Threads) -if (NOT MINGW AND NOT MSVC) +if (NOT MINGW AND NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") if (APPLE AND NOT MACOS_USE_DEPENDENCIES) execute_process(COMMAND brew --prefix icu4c OUTPUT_VARIABLE HOMEBREW_PREFIX_ICU OUTPUT_STRIP_TRAILING_WHITESPACE) # Needed for linking with non-broken icu on Apple platforms @@ -249,7 +261,7 @@ if (NOT OPENRCT2_COMMIT_SHA1_SHORT STREQUAL "HEAD" AND NOT OPENRCT2_COMMIT_SHA1_ OPENRCT2_COMMIT_SHA1_SHORT="${OPENRCT2_COMMIT_SHA1_SHORT}") endif() -if((X86 OR X86_64) AND NOT MSVC) +if((X86 OR X86_64) AND NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/SSE41Drawing.cpp PROPERTIES COMPILE_FLAGS -msse4.1) set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/AVX2Drawing.cpp PROPERTIES COMPILE_FLAGS -mavx2) endif() diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index fbad4d5851..7d3ff955f5 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -1201,9 +1201,21 @@ namespace OpenRCT2 { SwitchToStartUpScene(); } - +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop_arg( + [](void* vctx) { + auto ctx = reinterpret_cast(vctx); + if (ctx->_finished) + { + emscripten_cancel_main_loop(); + } + ctx->RunFrame(); + }, + this, 0, 1); +#else _stdInOutConsole.Start(); RunGameLoop(); +#endif } bool ShouldDraw() @@ -1229,6 +1241,7 @@ namespace OpenRCT2 /** * Run the main game loop until the finished flag is set. */ +#ifndef __EMSCRIPTEN__ void RunGameLoop() { PROFILED_FUNCTION(); @@ -1236,22 +1249,14 @@ namespace OpenRCT2 LOG_VERBOSE("begin openrct2 loop"); _finished = false; -#ifndef __EMSCRIPTEN__ _variableFrame = ShouldRunVariableFrame(); do { RunFrame(); } while (!_finished); -#else - emscripten_set_main_loop_arg( - [](void* vctx) -> { - auto ctx = reinterpret_cast(vctx); - ctx->RunFrame(); - }, - this, 0, 1); -#endif // __EMSCRIPTEN__ LOG_VERBOSE("finish openrct2 loop"); } +#endif // __EMSCRIPTEN__ void RunFrame() { diff --git a/src/openrct2/Version.h b/src/openrct2/Version.h index 40943d6a93..afd8128967 100644 --- a/src/openrct2/Version.h +++ b/src/openrct2/Version.h @@ -35,8 +35,10 @@ #elif defined(__riscv) #define OPENRCT2_ARCHITECTURE "RISC-V" #endif -#ifdef __EMSCRIPTEN__ - #define OPENRCT2_ARCHITECTURE "Emscripten" +#ifdef __wasm32__ + #define OPENRCT2_ARCHITECTURE "wasm32" +#elif defined(__wasm64__) + #define OPENRCT2_ARCHITECTURE "wasm64" #endif #ifndef OPENRCT2_ARCHITECTURE diff --git a/src/openrct2/core/Imaging.cpp b/src/openrct2/core/Imaging.cpp index 363a6baac3..8ddbffde81 100644 --- a/src/openrct2/core/Imaging.cpp +++ b/src/openrct2/core/Imaging.cpp @@ -26,6 +26,12 @@ #include #include +#ifdef __EMSCRIPTEN__ + #include + #include + #include +#endif + namespace OpenRCT2::Imaging { constexpr auto EXCEPTION_IMAGE_FORMAT_UNKNOWN = "Unknown image format."; @@ -334,8 +340,27 @@ namespace OpenRCT2::Imaging break; case IMAGE_FORMAT::PNG: { +#ifndef __EMSCRIPTEN__ std::ofstream fs(fs::u8path(path), std::ios::binary); WritePng(fs, image); +#else + std::ostringstream stream(std::ios::binary); + WritePng(stream, image); + std::string dataStr = stream.str(); + void* data = reinterpret_cast(dataStr.data()); + MAIN_THREAD_EM_ASM( + { + const a = document.createElement("a"); + // Blob requires the data must not be shared + const data = new Uint8Array(HEAPU8.subarray($0, $0 + $1)); + a.href = URL.createObjectURL(new Blob([data])); + a.download = UTF8ToString($2).split("/").pop(); + a.click(); + setTimeout(function(){ URL.revokeObjectURL(a.href) }, 1000); + }, + data, dataStr.size(), std::string(path).c_str()); + free(data); +#endif break; } default: diff --git a/src/openrct2/core/Money.hpp b/src/openrct2/core/Money.hpp index f31f908f1c..4a3223f239 100644 --- a/src/openrct2/core/Money.hpp +++ b/src/openrct2/core/Money.hpp @@ -24,7 +24,7 @@ using money64 = fixed64_1dp; // really tries to use a gigantic constant that can't fit in a double, they are // probably going to be breaking other things anyways. // For more details, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=26374 -constexpr money64 operator"" _GBP(long double money) noexcept +constexpr money64 operator""_GBP(long double money) noexcept { return static_cast(money) * 10; } diff --git a/src/openrct2/core/Speed.hpp b/src/openrct2/core/Speed.hpp index 575760c2fe..725710a338 100644 --- a/src/openrct2/core/Speed.hpp +++ b/src/openrct2/core/Speed.hpp @@ -12,7 +12,7 @@ #include // Note: Only valid for 5 decimal places. -constexpr int32_t operator"" _mph(long double speedMph) +constexpr int32_t operator""_mph(long double speedMph) { uint32_t wholeNumber = speedMph; uint64_t fraction = (speedMph - wholeNumber) * 100000; diff --git a/src/openrct2/platform/Platform.Linux.cpp b/src/openrct2/platform/Platform.Linux.cpp index f58fa97a46..32363dc900 100644 --- a/src/openrct2/platform/Platform.Linux.cpp +++ b/src/openrct2/platform/Platform.Linux.cpp @@ -158,7 +158,7 @@ namespace OpenRCT2::Platform { LOG_FATAL("failed to get process path"); } - #elif defined(__OpenBSD__) + #elif defined(__OpenBSD__) || defined(__EMSCRIPTEN__) // There is no way to get the path name of a running executable. // If you are not using the port or package, you may have to change this line! strlcpy(exePath, "/usr/local/bin/", sizeof(exePath)); diff --git a/src/openrct2/platform/Platform.Posix.cpp b/src/openrct2/platform/Platform.Posix.cpp index 6f043f737c..b4ae18d45c 100644 --- a/src/openrct2/platform/Platform.Posix.cpp +++ b/src/openrct2/platform/Platform.Posix.cpp @@ -161,7 +161,7 @@ namespace OpenRCT2::Platform // Return exit code return pclose(fpipe); #else - LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", command.c_str()); + LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", std::string(command).c_str()); return -1; #endif // __EMSCRIPTEN__ } diff --git a/src/openrct2/ride/VehicleRiderControl.cpp b/src/openrct2/ride/VehicleRiderControl.cpp index b6990daa6a..907a03a4c9 100644 --- a/src/openrct2/ride/VehicleRiderControl.cpp +++ b/src/openrct2/ride/VehicleRiderControl.cpp @@ -13,7 +13,7 @@ using namespace OpenRCT2; -constexpr int operator"" _MPH(unsigned long long x) noexcept +constexpr int operator""_MPH(unsigned long long x) noexcept { return x * 29127; }