Merge pull request #23515 from ethanaobrien/develop

Fix emscripten (webassembly) support
This commit is contained in:
Michał Janiszewski 2025-01-21 17:14:03 +01:00 committed by GitHub
commit a002834f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 684 additions and 31 deletions

View file

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

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

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

117
emscripten/deps.js Normal file
View file

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

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>

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

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

36
scripts/build-emscripten Executable file
View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,10 @@
#include <openrct2/platform/Platform.h>
#include <openrct2/ui/UiContext.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#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<IContext> context;
int32_t rc = EXIT_SUCCESS;
int runGame = CommandLineRun(argv, argc);

View file

@ -27,6 +27,10 @@
#include <stdexcept>
#include <unistd.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#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

View file

@ -48,6 +48,11 @@
#include <openrct2/world/Location.hpp>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,6 +45,14 @@
#include <openrct2/ui/UiContext.h>
#include <openrct2/ui/WindowManager.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
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
@ -397,6 +409,10 @@ namespace OpenRCT2::Ui::Windows
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
#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<const Widget> 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
}
}

View file

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

View file

@ -1200,9 +1200,21 @@ namespace OpenRCT2
{
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();
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<Context*>(vctx);
ctx->RunFrame();
},
this, 0, 1);
#endif // __EMSCRIPTEN__
LOG_VERBOSE("finish openrct2 loop");
}
#endif // __EMSCRIPTEN__
void RunFrame()
{

View file

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

View file

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

View file

@ -26,6 +26,12 @@
#include <stdexcept>
#include <unordered_map>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <iostream>
#include <sstream>
#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<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;
}
default:

View file

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

View file

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