1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2025-12-10 09:32:29 +01:00

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

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