diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 87f66a0859..53a595b551 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -627,6 +627,29 @@ jobs:
name: OpenRCT2-${{ needs.build_variables.outputs.name }}-Android
path: artifacts
if-no-files-found: error
+ emscripten:
+ name: Emscripten
+ runs-on: ubuntu-latest
+ needs: [check-code-formatting, build_variables]
+ container: openrct2/openrct2-build:19-emscripten
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: ccache
+ uses: hendrikmuhs/ccache-action@v1.2.13
+ with:
+ key: emscripten
+ - name: Install GCC problem matcher
+ uses: ammaraskar/gcc-problem-matcher@master
+ - name: Build OpenRCT2
+ run: |
+ . scripts/setenv
+ build-emscripten
+ - name: Upload artifacts (CI)
+ uses: actions/upload-artifact@v4
+ with:
+ path: build/www
+ name: OpenRCT2-${{ needs.build_variables.outputs.name }}-emscripten
release:
name: Release
runs-on: ubuntu-latest
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 909181fc5a..7bc26d4c2c 100644
--- a/data/language/en-GB.txt
+++ b/data/language/en-GB.txt
@@ -3792,3 +3792,5 @@ STR_6726 :Y:
STR_6727 :Dive Loop (left)
STR_6728 :Dive Loop (right)
STR_6729 :Cable lift hill must start immediately after station or block brake
+STR_6730 :Export emscripten data
+STR_6731 :Import emscripten data
diff --git a/emscripten/deps.js b/emscripten/deps.js
new file mode 100644
index 0000000000..2967b9e6a5
--- /dev/null
+++ b/emscripten/deps.js
@@ -0,0 +1,117 @@
+/*****************************************************************************
+ * 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.
+ *****************************************************************************/
+var EmscriptenDeps = {
+ ExportPersistentData: () =>
+ {
+ if (!window.JSZip)
+ {
+ alert("JSZip library not found. Aborting");
+ return;
+ }
+ const 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;
+ }
+ const zip = zipFolder("/persistent/");
+ 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);
+ })
+ },
+ ImportPersistentData: () =>
+ {
+ if (!window.JSZip)
+ {
+ alert("JSZip library not found. Aborting");
+ return;
+ }
+ const clearDatabase = async(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));
+ };
+ 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("/persistent/");
+ 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();
+ }
+};
+
+mergeInto(LibraryManager.library, EmscriptenDeps);
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
+
+
Please select your RCT2 assets (zip file):
+
+
+
+
+
+
diff --git a/emscripten/static/index.js b/emscripten/static/index.js
new file mode 100644
index 0000000000..7eab066565
--- /dev/null
+++ b/emscripten/static/index.js
@@ -0,0 +1,231 @@
+/*****************************************************************************
+ * 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.
+ *****************************************************************************/
+(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.";
+ }
+ if (!window.WebAssembly)
+ {
+ document.getElementById("loadingWebassembly").innerText = "Error! This page requires WebAssembly. Please upgrade your browser or enable WebAssembly support.";
+ }
+
+ 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;
+ }
+ });
+
+ Module.FS.mkdir("/persistent");
+ Module.FS.mount(Module.FS.filesystems.IDBFS, {autoPersist: true}, '/persistent');
+
+ 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("/persistent/config.ini");
+ if (!configExists)
+ {
+ Module.FS.writeFile("/persistent/config.ini", `
+[general]
+game_path = "/RCT"
+uncap_fps = true
+window_scale = 1.750000
+`);
+ }
+
+ const assetsOK = await updateAssets();
+ if (!assetsOK)
+ {
+ return
+ }
+
+ 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 /persistent.
+
+ALWAYS be sure to save to /persistent/saves when saving a game! Otherwise it will be wiped!
+
+You can import/export the /persistent 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=/persistent/", "--openrct2-data-path=/OpenRCT2/"]);
+})();
+
+async function updateAssets() {
+ let currentVersion = "";
+ try {
+ currentVersion = Module.FS.readFile("/OpenRCT2/version", {encoding: "utf8"});
+ console.log("Found asset version", currentVersion);
+ } catch(e) {
+ console.log("No asset version found");
+ };
+ let assetsVersion = "DEBUG";
+ try {
+ assetsVersion = Module.ccall("GetVersion", "string");
+ } catch(e) {
+ console.warn("Could not call 'GetVersion'! Is it added to EXPORTED_FUNCTIONS? Is ccall added to EXPORTED_RUNTIME_METHODS?");
+ };
+
+ //Always pull assets on a debug build
+ if (currentVersion !== assetsVersion || assetsVersion.includes("DEBUG"))
+ {
+ console.log("Updating assets to", assetsVersion);
+ document.getElementById("loadingWebassembly").innerText = "Asset update found. Downloading...";
+ await clearDatabase("/OpenRCT2/");
+
+ // Fetch the assets.zip file
+ const response = await fetch("assets.zip");
+ if (!response.ok) {
+ if (response.status === 404) {
+ document.getElementById("loadingWebassembly").innerText = "Error! Assets file not found (404).";
+ } else {
+ document.getElementById("loadingWebassembly").innerText = `Error! Failed to download assets (status: ${response.status}).`;
+ }
+ return false;
+ } else {
+ document.getElementById("loadingWebassembly").innerText = "Downloaded assets.zip";
+ }
+
+ await extractZip(await response.blob(), () => {
+ return "/OpenRCT2/";
+ });
+ Module.FS.writeFile("/OpenRCT2/version", assetsVersion.toString());
+ }
+ return true;
+}
+
+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 fileExists(path) {
+ try {
+ Module.FS.readFile(path);
+ return true;
+ } catch(e) {};
+ return false;
+}
diff --git a/scripts/build-emscripten b/scripts/build-emscripten
new file mode 100755
index 0000000000..d85c3cf4fe
--- /dev/null
+++ b/scripts/build-emscripten
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+mkdir -p build
+cd build
+
+START_DIR=$(pwd)
+SPEEXDSP_ROOT=/ext/speexdsp
+ICU_ROOT=/ext/icu/icu4c/source
+LIBZIP_ROOT=/ext/libzip
+JSON_DIR=/usr/include/nlohmann/
+
+emcmake cmake ../ \
+ -G Ninja \
+ -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="$SPEEXDSP_ROOT/include/" \
+ -DSPEEXDSP_LIBRARY="$SPEEXDSP_ROOT/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="$LIBZIP_ROOT/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=ccall,FS,callMain,UTF8ToString,stringToNewUTF8 -lidbfs.js --use-preload-plugins -s MODULARIZE=1 -s 'EXPORT_NAME=\"OPENRCT2_WEB\"'"
+
+emmake ninja
+
+rm -rf www/
+mkdir -p www/
+cd www/
+cp -r ../openrct2.* ./
+cp -r ../../emscripten/static/* ./
diff --git a/src/openrct2-cli/CMakeLists.txt b/src/openrct2-cli/CMakeLists.txt
index 0c0d258704..53c4677d57 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 (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..93b498abbb 100644
--- a/src/openrct2-ui/CMakeLists.txt
+++ b/src/openrct2-ui/CMakeLists.txt
@@ -11,7 +11,16 @@ option(DISABLE_VORBIS "Disable OGG/VORBIS support.")
option(DISABLE_OPENGL "Disable OpenGL support.")
# Third party libraries
-if (MSVC)
+if (EMSCRIPTEN)
+ set(USE_FLAGS "${EMSCRIPTEN_FLAGS}")
+ if (NOT DISABLE_VORBIS)
+ set(USE_FLAGS "${USE_FLAGS} -s USE_VORBIS=1 -s USE_OGG=1")
+ endif ()
+ set(SHARED_FLAGS "-fexceptions")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS} -s EXPORTED_FUNCTIONS=_GetVersion,_main --js-library ${ROOT_DIR}/emscripten/deps.js")
+ find_package(SpeexDSP REQUIRED)
+elseif (MSVC)
find_package(SDL2 REQUIRED)
find_library(SPEEX_LDFLAGS libspeexdsp)
if (NOT DISABLE_FLAC)
@@ -33,7 +42,7 @@ else ()
endif ()
endif ()
-if (NOT DISABLE_OPENGL)
+if (NOT DISABLE_OPENGL AND NOT 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 +70,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 (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)
@@ -81,7 +95,7 @@ if (NOT DISABLE_FLAC)
endif ()
if (NOT DISABLE_VORBIS)
- if (NOT MSVC AND NOT WIN32)
+ if (NOT MSVC AND NOT WIN32 AND NOT EMSCRIPTEN)
target_link_libraries(${PROJECT_NAME} PkgConfig::OGG PkgConfig::VORBISFILE)
else ()
target_link_libraries(${PROJECT_NAME} ${OGG_LDFLAGS} ${VORBISFILE_LDFLAGS})
diff --git a/src/openrct2-ui/TextComposition.cpp b/src/openrct2-ui/TextComposition.cpp
index 19be2b7c79..1b03f3cba6 100644
--- a/src/openrct2-ui/TextComposition.cpp
+++ b/src/openrct2-ui/TextComposition.cpp
@@ -171,7 +171,7 @@ void TextComposition::HandleMessage(const SDL_Event* e)
case SDLK_c:
if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && _session.Length)
{
- SDL_SetClipboardText(_session.Buffer->c_str());
+ OpenRCT2::GetContext()->GetUiContext()->SetClipboardText(_session.Buffer->c_str());
ContextShowError(STR_COPY_INPUT_TO_CLIPBOARD, kStringIdNone, {});
}
break;
diff --git a/src/openrct2-ui/Ui.cpp b/src/openrct2-ui/Ui.cpp
index 2674fa7528..214fbe15f6 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", function(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 ba0d4b829a..5bae0b072b 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 f7938e6a92..5c2a0dfda4 100644
--- a/src/openrct2-ui/UiContext.cpp
+++ b/src/openrct2-ui/UiContext.cpp
@@ -48,6 +48,11 @@
#include
#include
+#ifdef __EMSCRIPTEN__
+ #include
+ #include
+#endif
+
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Scripting;
@@ -712,7 +717,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;
+ };
+ },
+ target)
+ == 0);
+#endif
}
ITitleSequencePlayer* GetTitleSequencePlayer() override
@@ -752,9 +775,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 1f22359873..17b476d976 100644
--- a/src/openrct2-ui/UiStringIds.h
+++ b/src/openrct2-ui/UiStringIds.h
@@ -1103,6 +1103,8 @@ namespace OpenRCT2
STR_DRAWING_ENGINE_TIP = 5876,
STR_EARLY_COMPLETION_TIP = 6227,
STR_EDIT_ASSET_PACKS_BUTTON = 6640,
+ STR_EXPORT_EMSCRIPTEN = 6730,
+ STR_IMPORT_EMSCRIPTEN = 6731,
STR_EDIT_THEMES_BUTTON = 5153,
STR_EDIT_THEMES_BUTTON_TIP = 5837,
STR_EFFECTS_GROUP = 6256,
@@ -2272,4 +2274,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 424ac1b438..80a228904e 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 52951d795f..cef31e690a 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 79b5faca71..c6ad8aadb4 100644
--- a/src/openrct2-ui/input/MouseInput.cpp
+++ b/src/openrct2-ui/input/MouseInput.cpp
@@ -613,6 +613,7 @@ namespace OpenRCT2
}
}
+#ifndef __EMSCRIPTEN__
const CursorState* cursorState = ContextGetCursorState();
if (cursorState->touch || Config::Get().general.InvertViewportDrag)
{
@@ -622,6 +623,9 @@ namespace OpenRCT2
{
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 276ddb8972..63ab9710ca 100644
--- a/src/openrct2-ui/windows/About.cpp
+++ b/src/openrct2-ui/windows/About.cpp
@@ -114,7 +114,7 @@ namespace OpenRCT2::Ui::Windows
ContextOpenWindowView(WV_NEW_VERSION_INFO);
break;
case WIDX_COPY_BUILD_INFO:
- SDL_SetClipboardText(gVersionInfoFull);
+ OpenRCT2::GetContext()->GetUiContext()->SetClipboardText(gVersionInfoFull);
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 a101dce016..fc53f6b090 100644
--- a/src/openrct2-ui/windows/Options.cpp
+++ b/src/openrct2-ui/windows/Options.cpp
@@ -45,6 +45,14 @@
#include
#include
+#ifdef __EMSCRIPTEN__
+ #include
+extern "C" {
+extern void ExportPersistentData();
+extern void ImportPersistentData();
+}
+#endif
+
using namespace OpenRCT2;
using namespace OpenRCT2::Audio;
@@ -217,6 +225,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
@@ -392,11 +404,15 @@ namespace OpenRCT2::Ui::Windows
MakeWidget ({165, 113}, {135, 13}, WindowWidgetType::DropdownMenu, WindowColour::Secondary ), // Autosave dropdown
MakeWidget ({288, 114}, { 11, 11}, WindowWidgetType::Button, WindowColour::Secondary, STR_DROPDOWN_GLYPH, STR_AUTOSAVE_FREQUENCY_TIP ), // Autosave dropdown button
MakeWidget ({ 23, 130}, {135, 12}, WindowWidgetType::Label, WindowColour::Secondary, STR_AUTOSAVE_AMOUNT, STR_AUTOSAVE_AMOUNT_TIP ),
- MakeSpinnerWidgets({165, 130}, {135, 12}, WindowWidgetType::Spinner, WindowColour::Secondary, kStringIdNone, STR_AUTOSAVE_AMOUNT_TIP ), // Autosave amount spinner
+ MakeSpinnerWidgets({165, 130}, {135, 12}, WindowWidgetType::Spinner, WindowColour::Secondary, kStringIdNone, STR_AUTOSAVE_AMOUNT_TIP ), // Autosave amount spinner
MakeWidget ({ 23, 145}, {276, 12}, WindowWidgetType::Label, WindowColour::Secondary, STR_PATH_TO_RCT1, STR_PATH_TO_RCT1_TIP ), // RCT 1 path text
- MakeWidget ({ 24, 160}, {266, 14}, WindowWidgetType::Button, WindowColour::Secondary, kStringIdNone, STR_STRING_TOOLTIP ), // RCT 1 path button
+ MakeWidget ({ 24, 160}, {266, 14}, WindowWidgetType::Button, WindowColour::Secondary, kStringIdNone, 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, kStringIdNone ), // Asset packs
+ MakeWidget ({150, 176}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EDIT_ASSET_PACKS_BUTTON, kStringIdNone ), // Asset packs
+#ifdef __EMSCRIPTEN__
+ MakeWidget ({150, 192}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_EXPORT_EMSCRIPTEN, kStringIdNone ), // Emscripten data export
+ MakeWidget ({150, 208}, {150, 14}, WindowWidgetType::Button, WindowColour::Secondary, STR_IMPORT_EMSCRIPTEN, kStringIdNone ), // Emscripten data import
+#endif
};
static constexpr std::span window_options_page_widgets[] = {
@@ -1962,6 +1978,14 @@ namespace OpenRCT2::Ui::Windows
case WIDX_ASSET_PACKS:
ContextOpenWindow(WindowClass::AssetPacks);
break;
+#ifdef __EMSCRIPTEN__
+ case WIDX_EXPORT_EMSCRIPTEN_DATA:
+ ExportPersistentData();
+ break;
+ case WIDX_IMPORT_EMSCRIPTEN_DATA:
+ ImportPersistentData();
+ break;
+#endif
}
}
diff --git a/src/openrct2/CMakeLists.txt b/src/openrct2/CMakeLists.txt
index 89dc426696..d150ad0f3e 100644
--- a/src/openrct2/CMakeLists.txt
+++ b/src/openrct2/CMakeLists.txt
@@ -114,7 +114,17 @@ if (NOT DISABLE_GOOGLE_BENCHMARK)
endif ()
# Third party libraries
-if (MSVC)
+if (EMSCRIPTEN)
+ target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${ICU_INCLUDE_DIR})
+ set(USE_FLAGS "${EMSCRIPTEN_FLAGS}")
+ if (NOT DISABLE_VORBIS)
+ set(USE_FLAGS "${USE_FLAGS} -s USE_VORBIS=1 -s USE_OGG=1")
+ endif ()
+ set(SHARED_FLAGS "-fexceptions")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${USE_FLAGS} ${SHARED_FLAGS}")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LDFLAGS} --bind ${SHARED_FLAGS}")
+ find_package(SpeexDSP REQUIRED)
+elseif (MSVC)
find_package(png 1.6 REQUIRED)
find_package(zlib REQUIRED)
@@ -142,7 +152,7 @@ if (STATIC)
${ZLIB_STATIC_LIBRARIES}
${LIBZIP_STATIC_LIBRARIES})
else ()
- if (NOT MSVC)
+ if (NOT MSVC AND NOT EMSCRIPTEN)
target_link_libraries(${PROJECT_NAME}
PkgConfig::PNG
PkgConfig::ZLIB
@@ -171,7 +181,12 @@ set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} Threads::Threads)
-if (NOT MINGW AND NOT MSVC)
+# For some reason, these flags break the check for pthreads. Add them after.
+if (EMSCRIPTEN)
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORTED_FUNCTIONS=_GetVersion,_main --js-library ${ROOT_DIR}/emscripten/deps.js")
+endif()
+
+if (NOT MINGW AND NOT MSVC AND NOT 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 +264,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 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 64b7e7b8ac..8238ffb749 100644
--- a/src/openrct2/Context.cpp
+++ b/src/openrct2/Context.cpp
@@ -1200,9 +1200,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()
@@ -1228,6 +1240,7 @@ namespace OpenRCT2
/**
* Run the main game loop until the finished flag is set.
*/
+#ifndef __EMSCRIPTEN__
void RunGameLoop()
{
PROFILED_FUNCTION();
@@ -1235,22 +1248,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.cpp b/src/openrct2/Version.cpp
index 1f528fed4c..49fcb39ac0 100644
--- a/src/openrct2/Version.cpp
+++ b/src/openrct2/Version.cpp
@@ -55,6 +55,16 @@ const char gVersionInfoFull[] = OPENRCT2_NAME ", "
#endif
;
+#ifdef __EMSCRIPTEN__
+// This must be wrapped in extern "C", according to the emscripten docs, "to prevent C++ name mangling"
+extern "C" {
+const char* GetVersion()
+{
+ return gVersionInfoFull;
+}
+}
+#endif
+
NewVersionInfo GetLatestVersion()
{
// If the check doesn't succeed, provide current version so we don't bother user
diff --git a/src/openrct2/Version.h b/src/openrct2/Version.h
index 02390cdeda..3487b52cb5 100644
--- a/src/openrct2/Version.h
+++ b/src/openrct2/Version.h
@@ -37,8 +37,10 @@
#elif defined(__loongarch__)
#define OPENRCT2_ARCHITECTURE "LoongArch"
#endif
-#ifdef __EMSCRIPTEN__
- #define OPENRCT2_ARCHITECTURE "Emscripten"
+#ifdef __wasm64__
+ #define OPENRCT2_ARCHITECTURE "wasm64"
+#elif defined(__wasm32__)
+ #define OPENRCT2_ARCHITECTURE "wasm32"
#endif
#ifndef OPENRCT2_ARCHITECTURE
diff --git a/src/openrct2/core/Imaging.cpp b/src/openrct2/core/Imaging.cpp
index a1dfc7857b..f3c5e93306 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
{
static constexpr auto kExceptionImageFormatUnknown = "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 60ef83b2e1..2af65b4a05 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 c38667217b..f1a14cf5d3 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 13af71fa03..7ceaa1a3cc 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 ada27c4d4f..ae24b045f6 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 8fd46f54d3..da83a10319 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;
}