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

+ + + + + 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; }