Files
EnderTheCoder 889ce945f2 v0.7.0: SDL-free wasm worker backend (WebGPU + WebGL2) with on-demand assets
The engine now runs on a plain Web Worker (no SDL, no PROXY_TO_PTHREAD: in a pthread Emscripten proxies every filesystem syscall to the main browser thread, where synchronous XHR - and thus FS.createLazyFile - is forbidden). A page/worker pair transfers an OffscreenCanvas and forwards DOM input; assets load lazily from an embedded manifest, so entering a map fetches only that map and its tiles.

Presentation: ra3.webgpu (WebGPU via Emscripten's emdawnwebgpu port, WGSL shaders, the default on wasm) and ra3.wasmgl (WebGL2, GLSL ES). The legacy SDL ra3.webgl backend is removed.
2026-09-29 12:08:07 +08:00

133 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Static file server with HTTP Range support, for the OpenRA3 wasm build.
Emscripten's lazy file reads stream assets back as byte ranges, so the server
must honour the ``Range`` header -- Python's stdlib ``http.server`` does not.
python3 apps/web/serve.py --root build/wasm/bin --port 8199
# then open http://localhost:8199/index.html
Serve ``--root`` containing ``index.html``, ``openra3.js``, ``openra3.wasm``,
``openra3.worker.js`` and ``assets.manifest.json``. The lazy asset tree is
mounted from ``--assets`` (its files are then fetched from ``/assets/<rel>`` on
demand); without ``--assets`` the tree is expected under ``--root`` itself.
"""
import argparse
import functools
import os
import re
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
_RANGE = re.compile(r"bytes=(\d*)-(\d*)$")
class RangeHandler(SimpleHTTPRequestHandler):
# Directory served for ``/assets/...`` (set in main); None means "under root".
assets_dir = None
def translate_path(self, path):
if self.assets_dir:
prefix = "/assets/"
if path == "/assets" or path.startswith(prefix):
rel = path[len(prefix):] if path.startswith(prefix) else ""
rel = rel.split("?", 1)[0].split("#", 1)[0]
parts = [p for p in rel.split("/") if p and p != ".."]
return os.path.join(self.assets_dir, *parts)
return super().translate_path(path)
# Cross-origin isolation (kept for SharedArrayBuffer-style features and the
# WebGPU/pthread paths). no-store keeps the dev loop honest: a stale
# openra3.js/.wasm in the browser cache hides new builds (the worker's
# importScripts especially).
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Cache-Control", "no-store, max-age=0")
super().end_headers()
def send_head(self):
header = self.headers.get("Range")
if not header:
return super().send_head()
path = self.translate_path(self.path)
if os.path.isdir(path):
return super().send_head()
try:
handle = open(path, "rb")
except OSError:
self.send_error(404, "File not found")
return None
match = _RANGE.match(header.strip())
if not match:
handle.close()
return super().send_head()
size = os.fstat(handle.fileno()).st_size
start = int(match.group(1)) if match.group(1) else 0
end = int(match.group(2)) if match.group(2) else size - 1
if start > end or start >= size:
handle.close()
self.send_error(416, "Requested Range Not Satisfiable")
return None
end = min(end, size - 1)
self.send_response(206)
self.send_header("Content-Type", self.guess_type(path))
self.send_header("Accept-Ranges", "bytes")
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
self.send_header("Content-Length", str(end - start + 1))
self.send_header("Cache-Control", "no-store")
self.end_headers()
handle.seek(start)
self._remaining = end - start + 1
return handle
def copyfile(self, source, outputfile):
remaining = getattr(self, "_remaining", None)
if remaining is None:
return super().copyfile(source, outputfile)
try:
while remaining > 0:
chunk = source.read(min(65536, remaining))
if not chunk:
break
outputfile.write(chunk)
remaining -= len(chunk)
finally:
self._remaining = None
def main() -> int:
parser = argparse.ArgumentParser(description="OpenRA3 wasm dev server")
parser.add_argument("--root", default="build/wasm/bin", help="directory to serve")
parser.add_argument("--assets", default=None, help="directory served for /assets (the lazy asset tree)")
parser.add_argument("--port", type=int, default=8199)
parser.add_argument("--bind", default="127.0.0.1")
args = parser.parse_args()
if not os.path.isdir(args.root):
print(f"root not found: {args.root}", file=sys.stderr)
return 1
if args.assets and not os.path.isdir(args.assets):
print(f"assets not found: {args.assets}", file=sys.stderr)
return 1
RangeHandler.assets_dir = os.path.abspath(args.assets) if args.assets else None
handler = functools.partial(RangeHandler, directory=args.root)
server = ThreadingHTTPServer((args.bind, args.port), handler)
print(f"serving {args.root} at http://{args.bind}:{args.port}/index.html")
if RangeHandler.assets_dir:
print(f" /assets -> {RangeHandler.assets_dir}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())