mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2025-12-10 09:32:29 +01:00
Fix emscripten support
This commit is contained in:
27
emscripten/Dockerfile
Normal file
27
emscripten/Dockerfile
Normal 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
162
emscripten/build_emscripten.sh
Executable 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!"
|
||||
30
emscripten/static/index.html
Normal file
30
emscripten/static/index.html
Normal 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
271
emscripten/static/index.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user