Files
OpenRA3/apps/web/serve.py
T
EnderTheCoder 70d4beca4f v0.6.0: WebAssembly build with a WebGL backend (playable in the browser)
- ra3.webgl: a Web peer of Vulkan/Direct3D (SDL3 canvas + GLES3/WebGL2), with
  2D blit and the GPU terrain raymarch; GLSL ES shaders; null fallback off
  Emscripten. backend::webgl + default_backend() (webgl under Emscripten).
- Emscripten toolchain + Dockerfile.wasm + scripts/build-wasm.sh; import std,
  global -fexceptions, and Asyncify so the blocking frame loop yields to the
  browser (display::sleep_frame calls emscripten_sleep).
- apps/web/shell.html + apps/web/serve.py (Range-capable dev server).
- Assets: browsers forbid synchronous on-demand reads on the main thread (and
  FS.createLazyFile / the WasmFS fetch backend are worker-only; SDL3's Emscripten
  backend is main-thread DOM only, so the engine cannot run in a worker). The
  wasm build therefore preloads a compact per-map set via OPENRA3_WEB_ASSETS.
- New openra3 textures --map ID lists the loose terrain TGAs a map resolves to
  (terrain::resolve_texture_files, shared with load_textures_from_dir).
- Fixes: webgl heightmap used GL_R16 = 0x8229 (that is R8) -> upload rejected,
  terrain flattened to water; now 0x822A. Logger uses a stdout console sink on
  the web (stderr maps to console.error); a GL error check logs bad uploads.
- CI: wasm image + build jobs.
2026-09-28 22:57:19 +08:00

108 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Static file server with HTTP Range support, for the OpenRA3 wasm build.
Emscripten's lazy MEMFS reads 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/openra3.html
Serve ``--root`` containing ``openra3.html/.js/.wasm``, ``assets.manifest.json``
and the extracted ``assets/`` tree (a directory or a junction/symlink to it).
"""
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):
# SharedArrayBuffer (needed by the pthreads build) requires cross-origin
# isolation.
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
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("--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
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}/openra3.html")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())