Fix emscripten support

This commit is contained in:
Ethan O'Brien 2024-12-31 09:31:33 -06:00
parent 752f169acf
commit 63b0106de8
28 changed files with 769 additions and 25 deletions

14
.gitignore vendored
View file

@ -310,3 +310,17 @@ discord-rpc
# CMake # CMake
cmake-build-*/ 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/

40
cmake/FindSpeexDSP.cmake Normal file
View file

@ -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)

View file

@ -3787,3 +3787,5 @@ STR_6709 :Enter Smooth Strength between {COMMA16} and {COMMA16}
STR_6710 :Stable sort STR_6710 :Stable sort
STR_6711 :Filename: STR_6711 :Filename:
STR_6712 :Save STR_6712 :Save
STR_6713 :Export emscripten data
STR_6714 :Import emscripten data

27
emscripten/Dockerfile Normal file
View file

@ -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/* .

162
emscripten/build_emscripten.sh Executable file
View file

@ -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!"

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>OpenRCT2</title>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js" crossorigin=anonymous></script>
<script src="openrct2.js"></script>
<style>
* { padding: 0; margin: 0; }
canvas {
display: block;
margin: 0 auto;
position: fixed;
left: 0;
right: 0;
width: 100%;
height: 100%;
}
</style>
<script src="index.js"></script>
</head>
<body>
<p id="loadingWebassembly">Please wait... Loading webassembly</p>
<div id="beforeLoad" style="display:none;">
<p id="statusMsg">Please select your RCT2 assets (zip file):</p>
<input type="file" id="selectFile"></input>
</div>
<canvas id="canvas" style="display:none;"></canvas>
</body>
</html>

271
emscripten/static/index.js Normal file
View file

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

View file

@ -17,5 +17,8 @@ file(GLOB_RECURSE OPENRCT2_CLI_SOURCES
add_executable(${PROJECT_NAME} ${OPENRCT2_CLI_SOURCES}) add_executable(${PROJECT_NAME} ${OPENRCT2_CLI_SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/..") target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/..")
ipo_set_target_properties(${PROJECT_NAME}) 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_libraries(${PROJECT_NAME} libopenrct2 Threads::Threads)
target_link_platform_libraries(${PROJECT_NAME}) target_link_platform_libraries(${PROJECT_NAME})

View file

@ -11,7 +11,18 @@ option(DISABLE_VORBIS "Disable OGG/VORBIS support.")
option(DISABLE_OPENGL "Disable OpenGL support.") option(DISABLE_OPENGL "Disable OpenGL support.")
# Third party libraries # 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_package(SDL2 REQUIRED)
find_library(SPEEX_LDFLAGS libspeexdsp) find_library(SPEEX_LDFLAGS libspeexdsp)
if (NOT DISABLE_FLAC) if (NOT DISABLE_FLAC)
@ -33,7 +44,7 @@ else ()
endif () endif ()
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. # GL doesn't work nicely with macOS, while find_package doesn't work with multiarch on Ubuntu.
if (APPLE) if (APPLE)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
@ -61,7 +72,12 @@ SET_CHECK_CXX_FLAGS(${PROJECT_NAME})
ipo_set_target_properties(${PROJECT_NAME}) ipo_set_target_properties(${PROJECT_NAME})
# mingw builds cannot use the PkgConfig imported targets # 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" target_link_libraries(${PROJECT_NAME} "libopenrct2"
PkgConfig::SDL2 PkgConfig::SDL2
PkgConfig::SPEEX) PkgConfig::SPEEX)

View file

@ -19,6 +19,10 @@
#include <openrct2/core/String.hpp> #include <openrct2/core/String.hpp>
#include <openrct2/core/UTF8.h> #include <openrct2/core/UTF8.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifdef __MACOSX__ #ifdef __MACOSX__
// macOS uses COMMAND rather than CTRL for many keyboard shortcuts // macOS uses COMMAND rather than CTRL for many keyboard shortcuts
#define KEYBOARD_PRIMARY_MODIFIER KMOD_GUI #define KEYBOARD_PRIMARY_MODIFIER KMOD_GUI
@ -170,7 +174,21 @@ void TextComposition::HandleMessage(const SDL_Event* e)
case SDLK_c: case SDLK_c:
if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && _session.Length) if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && _session.Length)
{ {
#ifndef __EMSCRIPTEN__
SDL_SetClipboardText(_session.Buffer->c_str()); 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, {}); ContextShowError(STR_COPY_INPUT_TO_CLIPBOARD, STR_NONE, {});
} }
break; break;

View file

@ -24,6 +24,10 @@
#include <openrct2/platform/Platform.h> #include <openrct2/platform/Platform.h>
#include <openrct2/ui/UiContext.h> #include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
using namespace OpenRCT2; using namespace OpenRCT2;
using namespace OpenRCT2::Audio; using namespace OpenRCT2::Audio;
using namespace OpenRCT2::Ui; using namespace OpenRCT2::Ui;
@ -43,6 +47,12 @@ int NormalisedMain(int argc, const char** argv)
int main(int argc, const char** argv) int main(int argc, const char** argv)
#endif #endif
{ {
#ifdef __EMSCRIPTEN__
MAIN_THREAD_EM_ASM({
specialHTMLTargets["!canvas"] = Module.canvas;
Module.canvas.addEventListener("contextmenu", (e) = > { e.preventDefault(); });
});
#endif
std::unique_ptr<IContext> context; std::unique_ptr<IContext> context;
int32_t rc = EXIT_SUCCESS; int32_t rc = EXIT_SUCCESS;
int runGame = CommandLineRun(argv, argc); int runGame = CommandLineRun(argv, argc);

View file

@ -27,6 +27,10 @@
#include <stdexcept> #include <stdexcept>
#include <unistd.h> #include <unistd.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace OpenRCT2::Ui namespace OpenRCT2::Ui
{ {
enum class DIALOG_TYPE enum class DIALOG_TYPE
@ -129,8 +133,12 @@ namespace OpenRCT2::Ui
void OpenURL(const std::string& url) override void OpenURL(const std::string& url) override
{ {
#ifndef __EMSCRIPTEN__
std::string cmd = String::stdFormat("xdg-open %s", url.c_str()); std::string cmd = String::stdFormat("xdg-open %s", url.c_str());
Platform::Execute(cmd); 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 std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override

View file

@ -49,6 +49,11 @@
#include <openrct2/world/Location.hpp> #include <openrct2/world/Location.hpp>
#include <vector> #include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
using namespace OpenRCT2; using namespace OpenRCT2;
using namespace OpenRCT2::Drawing; using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Scripting; using namespace OpenRCT2::Scripting;
@ -713,7 +718,25 @@ public:
bool SetClipboardText(const utf8* target) override bool SetClipboardText(const utf8* target) override
{ {
#ifndef __EMSCRIPTEN__
return (SDL_SetClipboardText(target) == 0); 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 ITitleSequencePlayer* GetTitleSequencePlayer() override
@ -753,9 +776,19 @@ private:
void CreateWindow(const ScreenCoordsXY& windowPos) 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 // Get saved window size
int32_t width = Config::Get().general.WindowWidth; int32_t width = Config::Get().general.WindowWidth;
int32_t height = Config::Get().general.WindowHeight; int32_t height = Config::Get().general.WindowHeight;
#endif
if (width <= 0) if (width <= 0)
width = 640; width = 640;
if (height <= 0) if (height <= 0)

View file

@ -1115,6 +1115,10 @@ namespace OpenRCT2
STR_DRAWING_ENGINE_TIP = 5876, STR_DRAWING_ENGINE_TIP = 5876,
STR_EARLY_COMPLETION_TIP = 6227, STR_EARLY_COMPLETION_TIP = 6227,
STR_EDIT_ASSET_PACKS_BUTTON = 6640, 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 = 5153,
STR_EDIT_THEMES_BUTTON_TIP = 5837, STR_EDIT_THEMES_BUTTON_TIP = 5837,
STR_EFFECTS_GROUP = 6256, STR_EFFECTS_GROUP = 6256,
@ -2282,4 +2286,4 @@ namespace OpenRCT2
STR_ADJUST_SMALLER_WATER_TIP = 2380, STR_ADJUST_SMALLER_WATER_TIP = 2380,
STR_WATER = 2383, STR_WATER = 2383,
}; };
} } // namespace OpenRCT2

View file

@ -42,7 +42,11 @@ OPENGL_PROC(PFNGLATTACHSHADERPROC, glAttachShader)
OPENGL_PROC(PFNGLBINDBUFFERPROC, glBindBuffer) OPENGL_PROC(PFNGLBINDBUFFERPROC, glBindBuffer)
OPENGL_PROC(PFNGLBINDFRAGDATALOCATIONPROC, glBindFragDataLocation) OPENGL_PROC(PFNGLBINDFRAGDATALOCATIONPROC, glBindFragDataLocation)
OPENGL_PROC(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer) OPENGL_PROC(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray) OPENGL_PROC(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray)
#else
extern "C" void glBindVertexArray(GLuint array);
#endif
OPENGL_PROC(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer) OPENGL_PROC(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer)
OPENGL_PROC(PFNGLBUFFERDATAPROC, glBufferData) OPENGL_PROC(PFNGLBUFFERDATAPROC, glBufferData)
OPENGL_PROC(PFNGLBUFFERSUBDATAPROC, glBufferSubData) OPENGL_PROC(PFNGLBUFFERSUBDATAPROC, glBufferSubData)
@ -55,7 +59,11 @@ OPENGL_PROC(PFNGLDELETEBUFFERSPROC, glDeleteBuffers)
OPENGL_PROC(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers) OPENGL_PROC(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers)
OPENGL_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram) OPENGL_PROC(PFNGLDELETEPROGRAMPROC, glDeleteProgram)
OPENGL_PROC(PFNGLDELETESHADERPROC, glDeleteShader) OPENGL_PROC(PFNGLDELETESHADERPROC, glDeleteShader)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays) OPENGL_PROC(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays)
#else
extern "C" void glDeleteVertexArrays(GLsizei n, const GLuint* arrays);
#endif
OPENGL_PROC(PFNGLDETACHSHADERPROC, glDetachShader) OPENGL_PROC(PFNGLDETACHSHADERPROC, glDetachShader)
OPENGL_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray) OPENGL_PROC(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray)
OPENGL_PROC(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D) OPENGL_PROC(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D)
@ -67,7 +75,11 @@ OPENGL_PROC(PFNGLGETPROGRAMIVPROC, glGetProgramiv)
OPENGL_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog) OPENGL_PROC(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog)
OPENGL_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv) OPENGL_PROC(PFNGLGETSHADERIVPROC, glGetShaderiv)
OPENGL_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation) OPENGL_PROC(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays) OPENGL_PROC(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays)
#else
extern "C" void glGenVertexArrays(GLsizei n, GLuint* arrays);
#endif
OPENGL_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram) OPENGL_PROC(PFNGLLINKPROGRAMPROC, glLinkProgram)
OPENGL_PROC(PFNGLSHADERSOURCEPROC, glShaderSource) OPENGL_PROC(PFNGLSHADERSOURCEPROC, glShaderSource)
OPENGL_PROC(PFNGLUNIFORM1IPROC, glUniform1i) OPENGL_PROC(PFNGLUNIFORM1IPROC, glUniform1i)
@ -82,6 +94,11 @@ OPENGL_PROC(PFNGLUNIFORM4FVPROC, glUniform4fv)
OPENGL_PROC(PFNGLUSEPROGRAMPROC, glUseProgram) OPENGL_PROC(PFNGLUSEPROGRAMPROC, glUseProgram)
OPENGL_PROC(PFNGLVERTEXATTRIBIPOINTERPROC, glVertexAttribIPointer) OPENGL_PROC(PFNGLVERTEXATTRIBIPOINTERPROC, glVertexAttribIPointer)
OPENGL_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer) OPENGL_PROC(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer)
#ifndef FAKE__EMSCRIPTEN__
OPENGL_PROC(PFNGLDRAWARRAYSINSTANCEDPROC, glDrawArraysInstanced) OPENGL_PROC(PFNGLDRAWARRAYSINSTANCEDPROC, glDrawArraysInstanced)
OPENGL_PROC(PFNGLVERTEXATTRIBDIVISORPROC, glVertexAttribDivisor) 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) OPENGL_PROC(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate)

View file

@ -70,7 +70,9 @@ private:
int32_t _drawCount = 0; int32_t _drawCount = 0;
#ifndef NO_TTF
uint32_t _ttfGlId = 0; uint32_t _ttfGlId = 0;
#endif
struct struct
{ {

View file

@ -605,6 +605,7 @@ static void InputViewportDragContinue()
} }
} }
#ifndef __EMSCRIPTEN__
const CursorState* cursorState = ContextGetCursorState(); const CursorState* cursorState = ContextGetCursorState();
if (cursorState->touch || Config::Get().general.InvertViewportDrag) if (cursorState->touch || Config::Get().general.InvertViewportDrag)
{ {
@ -614,6 +615,9 @@ static void InputViewportDragContinue()
{ {
ContextSetCursorPosition(gInputDragLast); ContextSetCursorPosition(gInputDragLast);
} }
#else
gInputDragLast = newDragCoords;
#endif
} }
static void InputViewportDragEnd() static void InputViewportDragEnd()

View file

@ -21,6 +21,10 @@
#include <openrct2/sprites.h> #include <openrct2/sprites.h>
#include <openrct2/ui/UiContext.h> #include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace OpenRCT2::Ui::Windows namespace OpenRCT2::Ui::Windows
{ {
static constexpr int32_t WW = 400; static constexpr int32_t WW = 400;
@ -119,7 +123,22 @@ namespace OpenRCT2::Ui::Windows
ContextOpenWindowView(WV_NEW_VERSION_INFO); ContextOpenWindowView(WV_NEW_VERSION_INFO);
break; break;
case WIDX_COPY_BUILD_INFO: case WIDX_COPY_BUILD_INFO:
#ifndef __EMSCRIPTEN__
SDL_SetClipboardText(gVersionInfoFull); SDL_SetClipboardText(gVersionInfoFull);
#else
MAIN_THREAD_EM_ASM(
{
try
{
navigator.clipboard.writeText(UTF8ToString($0));
}
catch (e)
{
// Ignore
};
},
gVersionInfoFull);
#endif
break; break;
case WIDX_CONTRIBUTORS_BUTTON: case WIDX_CONTRIBUTORS_BUTTON:
ContextOpenWindowView(WV_CONTRIBUTORS); ContextOpenWindowView(WV_CONTRIBUTORS);

View file

@ -46,6 +46,10 @@
#include <openrct2/sprites.h> #include <openrct2/sprites.h>
#include <openrct2/ui/UiContext.h> #include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
using namespace OpenRCT2; using namespace OpenRCT2;
using namespace OpenRCT2::Audio; using namespace OpenRCT2::Audio;
@ -218,6 +222,10 @@ namespace OpenRCT2::Ui::Windows
WIDX_PATH_TO_RCT1_BUTTON, WIDX_PATH_TO_RCT1_BUTTON,
WIDX_PATH_TO_RCT1_CLEAR, WIDX_PATH_TO_RCT1_CLEAR,
WIDX_ASSET_PACKS, WIDX_ASSET_PACKS,
#ifdef __EMSCRIPTEN__
WIDX_EXPORT_EMSCRIPTEN_DATA,
WIDX_IMPORT_EMSCRIPTEN_DATA,
#endif
}; };
// clang-format off // 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 ({ 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 ({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 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, kWidgetsEnd,
}; };
@ -1973,6 +1985,14 @@ namespace OpenRCT2::Ui::Windows
case WIDX_ASSET_PACKS: case WIDX_ASSET_PACKS:
ContextOpenWindow(WindowClass::AssetPacks); ContextOpenWindow(WindowClass::AssetPacks);
break; 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
} }
} }

View file

@ -114,7 +114,19 @@ if (NOT DISABLE_GOOGLE_BENCHMARK)
endif () endif ()
# Third party libraries # 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(png 1.6 REQUIRED)
find_package(zlib REQUIRED) find_package(zlib REQUIRED)
@ -142,7 +154,7 @@ if (STATIC)
${ZLIB_STATIC_LIBRARIES} ${ZLIB_STATIC_LIBRARIES}
${LIBZIP_STATIC_LIBRARIES}) ${LIBZIP_STATIC_LIBRARIES})
else () else ()
if (NOT MSVC) if (NOT MSVC AND NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PkgConfig::PNG PkgConfig::PNG
PkgConfig::ZLIB PkgConfig::ZLIB
@ -171,7 +183,7 @@ set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} Threads::Threads) 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) if (APPLE AND NOT MACOS_USE_DEPENDENCIES)
execute_process(COMMAND brew --prefix icu4c OUTPUT_VARIABLE HOMEBREW_PREFIX_ICU OUTPUT_STRIP_TRAILING_WHITESPACE) 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 # 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}") OPENRCT2_COMMIT_SHA1_SHORT="${OPENRCT2_COMMIT_SHA1_SHORT}")
endif() 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/SSE41Drawing.cpp PROPERTIES COMPILE_FLAGS -msse4.1)
set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/AVX2Drawing.cpp PROPERTIES COMPILE_FLAGS -mavx2) set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/drawing/AVX2Drawing.cpp PROPERTIES COMPILE_FLAGS -mavx2)
endif() endif()

View file

@ -1201,9 +1201,21 @@ namespace OpenRCT2
{ {
SwitchToStartUpScene(); SwitchToStartUpScene();
} }
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(
[](void* vctx) {
auto ctx = reinterpret_cast<Context*>(vctx);
if (ctx->_finished)
{
emscripten_cancel_main_loop();
}
ctx->RunFrame();
},
this, 0, 1);
#else
_stdInOutConsole.Start(); _stdInOutConsole.Start();
RunGameLoop(); RunGameLoop();
#endif
} }
bool ShouldDraw() bool ShouldDraw()
@ -1229,6 +1241,7 @@ namespace OpenRCT2
/** /**
* Run the main game loop until the finished flag is set. * Run the main game loop until the finished flag is set.
*/ */
#ifndef __EMSCRIPTEN__
void RunGameLoop() void RunGameLoop()
{ {
PROFILED_FUNCTION(); PROFILED_FUNCTION();
@ -1236,22 +1249,14 @@ namespace OpenRCT2
LOG_VERBOSE("begin openrct2 loop"); LOG_VERBOSE("begin openrct2 loop");
_finished = false; _finished = false;
#ifndef __EMSCRIPTEN__
_variableFrame = ShouldRunVariableFrame(); _variableFrame = ShouldRunVariableFrame();
do do
{ {
RunFrame(); RunFrame();
} while (!_finished); } while (!_finished);
#else
emscripten_set_main_loop_arg(
[](void* vctx) -> {
auto ctx = reinterpret_cast<Context*>(vctx);
ctx->RunFrame();
},
this, 0, 1);
#endif // __EMSCRIPTEN__
LOG_VERBOSE("finish openrct2 loop"); LOG_VERBOSE("finish openrct2 loop");
} }
#endif // __EMSCRIPTEN__
void RunFrame() void RunFrame()
{ {

View file

@ -35,8 +35,10 @@
#elif defined(__riscv) #elif defined(__riscv)
#define OPENRCT2_ARCHITECTURE "RISC-V" #define OPENRCT2_ARCHITECTURE "RISC-V"
#endif #endif
#ifdef __EMSCRIPTEN__ #ifdef __wasm32__
#define OPENRCT2_ARCHITECTURE "Emscripten" #define OPENRCT2_ARCHITECTURE "wasm32"
#elif defined(__wasm64__)
#define OPENRCT2_ARCHITECTURE "wasm64"
#endif #endif
#ifndef OPENRCT2_ARCHITECTURE #ifndef OPENRCT2_ARCHITECTURE

View file

@ -26,6 +26,12 @@
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <iostream>
#include <sstream>
#endif
namespace OpenRCT2::Imaging namespace OpenRCT2::Imaging
{ {
constexpr auto EXCEPTION_IMAGE_FORMAT_UNKNOWN = "Unknown image format."; constexpr auto EXCEPTION_IMAGE_FORMAT_UNKNOWN = "Unknown image format.";
@ -334,8 +340,27 @@ namespace OpenRCT2::Imaging
break; break;
case IMAGE_FORMAT::PNG: case IMAGE_FORMAT::PNG:
{ {
#ifndef __EMSCRIPTEN__
std::ofstream fs(fs::u8path(path), std::ios::binary); std::ofstream fs(fs::u8path(path), std::ios::binary);
WritePng(fs, image); WritePng(fs, image);
#else
std::ostringstream stream(std::ios::binary);
WritePng(stream, image);
std::string dataStr = stream.str();
void* data = reinterpret_cast<void*>(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; break;
} }
default: default:

View file

@ -158,7 +158,7 @@ namespace OpenRCT2::Platform
{ {
LOG_FATAL("failed to get process path"); 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. // 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! // If you are not using the port or package, you may have to change this line!
strlcpy(exePath, "/usr/local/bin/", sizeof(exePath)); strlcpy(exePath, "/usr/local/bin/", sizeof(exePath));

View file

@ -161,7 +161,7 @@ namespace OpenRCT2::Platform
// Return exit code // Return exit code
return pclose(fpipe); return pclose(fpipe);
#else #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; return -1;
#endif // __EMSCRIPTEN__ #endif // __EMSCRIPTEN__
} }