ioquake3.html replaces Emscripten-generated HTML shell

This enables several things:
* Optionally load pk3 files from a web server at runtime instead of bundling them with Emscripten at build time
* Set command line arguments via URL param
* It's not ugly
This commit is contained in:
James Darpinian 2024-06-07 08:27:56 -07:00 committed by Zack Middleton
parent e247b316a7
commit db24dfe13f
4 changed files with 151 additions and 18 deletions

View File

@ -1066,17 +1066,52 @@ ifeq ($(PLATFORM),emscripten)
CC=emcc
ARCH=wasm32
BINEXT=.js
# LDFLAGS+=-s MAIN_MODULE is needed for dlopen() in client/server but it causes compile errors
USE_RENDERER_DLOPEN=0
USE_OPENAL_DLOPEN=0
USE_CURL=0
HAVE_VM_COMPILED=false
BUILD_GAME_SO=0
BUILD_GAME_QVM=0
# Would be interesting to try to get the server working via WebRTC DataChannel.
# This would enable P2P play, hosting a server in the browser. Also,
# DataChannel is the only way to use UDP in the browser.
BUILD_SERVER=0
CLIENT_EXTRA_FILES+=code/web/ioquake3.html
CLIENT_CFLAGS+=-s USE_SDL=2
CLIENT_LDFLAGS+=-s TOTAL_MEMORY=256mb
CLIENT_LDFLAGS+=-s STACK_SIZE=5MB
# Informing Emscripten which WebGL versions we support makes the JS bundle smaller and faster to load.
CLIENT_LDFLAGS+=-s MIN_WEBGL_VERSION=2
CLIENT_LDFLAGS+=-s MAX_WEBGL_VERSION=2
CLIENT_LDFLAGS+=-s FULL_ES2=1
# The HTML file can use these functions to load extra files before the game starts.
CLIENT_LDFLAGS+=-s EXPORTED_RUNTIME_METHODS=FS,addRunDependency,removeRunDependency
CLIENT_LDFLAGS+=-s EXIT_RUNTIME=1
CLIENT_LDFLAGS+=-s EXPORT_ES6
CLIENT_LDFLAGS+=-s EXPORT_NAME=ioquake3
# Game data files can be packaged by emcc into a .data file that lives next to the wasm bundle
# and added to the virtual filesystem before the game starts. This requires the game data to be
# present at build time and it can't be changed afterward.
# For more flexibility, game data files can be loaded from a web server at runtime by listing
# them in ioq3-config.json. This way they don't have to be present at build time and can be
# changed later.
ifneq ($(wildcard $(BASEGAME)/*),)
CLIENT_LDFLAGS+=--preload-file $(BASEGAME)
EMSCRIPTEN_PRELOAD_FILE=1
CLIENT_EXTRA_FILES+=code/web/empty/ioq3-config.json
else
CLIENT_EXTRA_FILES+=code/web/$(BASEGAME)/ioq3-config.json
endif
BASE_CFLAGS=-fPIC -s USE_SDL=2
LDFLAGS=-s STACK_SIZE=5MB -s TOTAL_MEMORY=256MB -s MAX_WEBGL_VERSION=2 --preload-file $(BASEGAME)
OPTIMIZEVM = -O3
OPTIMIZE = $(OPTIMIZEVM) -ffast-math
FULLBINEXT=.html
SHLIBEXT=wasm
SHLIBCFLAGS=-fPIC
SHLIBLDFLAGS=-s SIDE_MODULE
@ -1129,19 +1164,16 @@ ifneq ($(BUILD_SERVER),0)
TARGETS += $(B)/$(SERVERBIN)$(FULLBINEXT)
ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(SERVERBIN).js \
$(B)/$(SERVERBIN).wasm \
$(B)/$(SERVERBIN).data
EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).wasm
ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1)
EMSCRIPTENOBJ+=$(B)/$(SERVERBIN).data
endif
endif
endif
ifneq ($(BUILD_CLIENT),0)
TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT)
ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(CLIENTBIN).js \
$(B)/$(CLIENTBIN).wasm \
$(B)/$(CLIENTBIN).data
ifneq ($(PLATFORM),emscripten)
TARGETS += $(B)/$(CLIENTBIN)$(FULLBINEXT)
endif
ifneq ($(USE_RENDERER_DLOPEN),0)
@ -1154,9 +1186,10 @@ ifneq ($(BUILD_CLIENT),0)
TARGETS += $(B)/$(CLIENTBIN)_opengl2$(FULLBINEXT)
ifeq ($(PLATFORM),emscripten)
EMSCRIPTENOBJ += $(B)/$(CLIENTBIN)_opengl2.js \
$(B)/$(CLIENTBIN)_opengl2.wasm \
$(B)/$(CLIENTBIN)_opengl2.data
EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.wasm
ifeq ($(EMSCRIPTEN_PRELOAD_FILE),1)
EMSCRIPTENOBJ+=$(B)/$(CLIENTBIN)_opengl2.wasm32.data
endif
endif
endif
endif
@ -1504,6 +1537,7 @@ ifneq ($(BUILD_CLIENT),0)
endif
NAKED_TARGETS=$(shell echo $(TARGETS) | sed -e "s!$(B)/!!g")
NAKED_EMSCRIPTENOBJ=$(shell echo $(EMSCRIPTENOBJ) | sed -e "s!$(B)/!!g")
print_list=-@for i in $(1); \
do \
@ -1559,7 +1593,20 @@ endif
@echo ""
@echo " Output:"
$(call print_list, $(NAKED_TARGETS))
$(call print_list, $(NAKED_EMSCRIPTENOBJ))
@echo ""
ifeq ($(PLATFORM),emscripten)
ifneq ($(EMSCRIPTEN_PRELOAD_FILE),1)
@echo " Warning: Game files not found in '$(BASEGAME)'."
@echo " They will not be packaged by Emscripten or preloaded."
@echo " To run this build you must serve the game files from a web server"
@echo " and list their paths in ioq3-config.json."
@echo " To make a build that automatically loads the game files, create a"
@echo " directory called '$(BASEGAME)' and copy your game files into it, then"
@echo " 'emmake make clean' and rebuild."
@echo ""
endif
endif
ifneq ($(TARGETS),)
ifndef DEBUG_MAKEFILE
@$(MAKE) $(TARGETS) $(B).zip V=$(V)
@ -1575,7 +1622,7 @@ endif
ifneq ($(PLATFORM),darwin)
ifdef ARCHIVE
@rm -f $@
@(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS))
@(cd $(B) && zip -r9 ../../$@ $(NAKED_TARGETS) $(NAKED_EMSCRIPTENOBJ))
endif
endif

View File

@ -0,0 +1,13 @@
{
"files": [
{"src": "baseq3/pak0.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak1.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak2.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak3.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak4.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak5.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak6.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak7.pk3", "dst": "/baseq3"},
{"src": "baseq3/pak8.pk3", "dst": "/baseq3"}
]
}

View File

@ -0,0 +1,3 @@
{
"files": []
}

70
code/web/ioquake3.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>ioquake3 Emscripten demo</title>
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: rgb(0, 0, 0); display:flex; align-items: center; justify-content: center; }
canvas { max-width: 100%; max-height: 100%; min-width: 100%; min-height: 100%; object-fit: contain; }
</style>
<canvas id=canvas></canvas>
<script type=module>
if (window.location.protocol === 'file:') throw new Error('Unfortunately browser security restrictions prevent loading wasm from a file: URL. This file must be loaded from a web server. The easiest way to do this is probably to use Python\'s built-in web server by running `python3 -m http.server` in the top level ioq3 directory and then navigating to http://localhost:8000/code/web/ioquake3.html');
// First set up the command line arguments and the Emscripten filesystem.
const urlParams = new URLSearchParams(window.location.search);
const basegame = urlParams.get('basegame') || 'baseq3';
let generatedArguments = `
+set net_enabled 0
+set r_mode -2
+set fs_game ${basegame}
`;
// Note that unfortunately "+" needs to be encoded as "%2b" in URL query strings or it will be stripped by the browser.
const queryArgs = urlParams.get('args');
if (queryArgs) generatedArguments += ` ${queryArgs} `;
// If buildPath is not specified, try to find a build in one of a few default paths.
let buildPath = urlParams.get('buildPath');
if (buildPath && !buildPath.endsWith('/')) buildPath += '/';
const buildPaths = buildPath ? [buildPath] : ['../../build/debug-emscripten-wasm32/', '../../build/release-emscripten-wasm32/', './'];
const scriptPaths = buildPaths.map(buildPath => buildPath + 'ioquake3_opengl2.wasm32.js');
const scriptResponses = await Promise.all(scriptPaths.map(p => fetch(p, {method: 'HEAD'})));
const validBuilds = scriptResponses.filter(r => r.ok).length;
const goodURL = (newPath) => {
const url = new URL(window.location);
url.searchParams.set('buildPath', newPath);
return url.toString().replace(/%2f/gi, '/');
};
if (validBuilds === 0) throw new Error(`Didn't find any wasm builds. Run \`emmake make debug\` to build one, or use the buildPath query parameter to specify a directory containing ioquake3_opengl2.wasm32.[js,wasm,data], e.g. ${goodURL('../../build/debug-emscripten-wasm32/')}`);
if (validBuilds > 1) throw new Error(`Found multiple valid builds at the following paths: [${buildPaths.filter((path, i)=>scriptResponses[i].ok)}]. Please specify which one to run by adding a buildPath query parameter to the URL, e.g. ${goodURL(buildPaths.filter((path, i)=>scriptResponses[i].ok)[0])}`);
const buildIndex = scriptResponses.findIndex(r => r.ok);
const selectedScript = scriptPaths[buildIndex];
buildPath = buildPaths[buildIndex];
const buildURL = new URL(buildPath, location.origin + location.pathname);
const configPromise = fetch(buildPath + 'ioq3-config.json').then(r => r.ok ? r.json() : {files: []});
const ioquake3 = (await import(selectedScript)).default;
ioquake3({
canvas: canvas,
arguments: generatedArguments.trim().split(/\s+/),
locateFile: (file) => buildPath + file,
preRun: [async (module) => {
module.addRunDependency('setup-ioq3-filesystem');
try {
const config = await configPromise;
const fetches = config.files.map(file => fetch(new URL(file.src, buildURL)));
for (let i = 0; i < config.files.length; i++) {
const response = await fetches[i];
if (!response.ok) continue;
const data = await response.arrayBuffer();
let name = config.files[i].src.match(/[^/]+$/)[0];
let dir = config.files[i].dst;
module.FS.mkdirTree(dir);
module.FS.writeFile(`${dir}/${name}`, new Uint8Array(data));
}
} finally {
module.removeRunDependency('setup-ioq3-filesystem');
}
}],
});
</script>