Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
4e1aa75
Add Emscripten/WebAssembly web port (TARGET_WEB=1)
zalo Mar 11, 2026
1f2dd8c
Fix Emscripten build: resolve compilation and runtime errors
zalo Mar 11, 2026
58564c8
Add Lua 5.3.6 source for Emscripten compilation
zalo Mar 11, 2026
55c1814
Get gameplay running: fix ROM loading, audio, and startup flow
zalo Mar 12, 2026
537d10d
Add follow-up features: language files, overlay fix, audio fix, proxy…
zalo Mar 12, 2026
da1b48d
Add host mode support via WebSocket proxy
zalo Mar 12, 2026
aaf7e4d
Fix game speed: decouple game tick rate from browser refresh rate
zalo Mar 12, 2026
ac83ef9
Add runtime 64-to-32 audio bank converter for web audio support
zalo Mar 12, 2026
a9f153a
Use page hostname for WebSocket proxy URL instead of hardcoded localhost
zalo Mar 12, 2026
59aeab8
Fix proxy: IPv4 UDP sockets and dynamic page hostname
zalo Mar 12, 2026
c15c4ea
Skip intro cutscene on web, fix proxy IPv4 bind
zalo Mar 12, 2026
6e483d1
Fix multiplayer disconnect: drain WebSocket buffer before timeout check
zalo Mar 12, 2026
c0480cb
Fix WebSocket disconnect: prevent main() from returning on web
zalo Mar 12, 2026
9aa8edd
Fix multiplayer disconnect and add URL auto-join/host
zalo Mar 12, 2026
5ecad5e
Fix level transition freeze: use full produce_one_frame path
zalo Mar 12, 2026
de4934a
Fix level transition freeze and frame pacing on web
zalo Mar 12, 2026
3ce7a27
Fix painting level transition crash: disable audio entirely on web
zalo Mar 12, 2026
8e0f9c5
Add ROM loading via URL parameter (#rom=URL or ?rom=URL)
zalo Mar 12, 2026
3227a69
Clean up debug diagnostics from level transition debugging
zalo Mar 12, 2026
8ef3d69
Remove debug stars and convert remaining printf to LOG_INFO
zalo Mar 12, 2026
1373fae
Clean up broken audio code, convert Lua to submodule
zalo Mar 12, 2026
1c74022
Add GitHub Actions workflow to build web and deploy to Pages
zalo Mar 12, 2026
fb5d94f
Cache Emscripten libs and build objects in CI
zalo Mar 12, 2026
3807717
Replace WebSocket+proxy networking with PeerJS WebRTC
zalo Mar 12, 2026
764bcea
Fix PeerJS: defer mod list request until connection is established
zalo Mar 12, 2026
e5d64a9
Fix PeerJS room auto-join: handle async host/client role detection
zalo Mar 12, 2026
0b7d86e
Fix PeerJS room auto-join: proper server→client transition
zalo Mar 12, 2026
013f8ac
Fix PeerJS client: full network_init(NT_CLIENT) for proper join
zalo Mar 12, 2026
fc767c9
Fix PeerJS room join: start as NT_CLIENT, transition to host if needed
zalo Mar 12, 2026
eed6791
Fix 3-player visibility: defer network init until PeerJS resolves role
zalo Mar 12, 2026
e276be5
Update DJUI join dialog for PeerJS room-based networking
zalo Mar 12, 2026
dc83e0d
Simplify PeerJS room join: back to djui_panel_do_host + simple role s…
zalo Mar 12, 2026
012c5a8
Fix URL room join: always start as NT_CLIENT, switch type for host
zalo Mar 12, 2026
c46d25c
Fix room join: host needs djui_panel_do_host for game session
zalo Mar 12, 2026
206fde7
Fix PeerJS host: register connection immediately, not in open handler
zalo Mar 12, 2026
16e0f52
Fix PeerJS host: wait for conn.open before registering for send
zalo Mar 12, 2026
1f576c4
Fix room join: poll PeerJS role before C network init
zalo Mar 12, 2026
c5db765
Persist player settings to IndexedDB via IDBFS
zalo Mar 13, 2026
2ff9a44
Fix IDBFS crash: mount in preRun instead of Asyncify
zalo Mar 13, 2026
f567390
Fix IDBFS: use localStorage instead to avoid hiding preloaded files
zalo Mar 13, 2026
7383429
Fix CI: clean stale .o files to prevent EM_ASM index mismatch
zalo Mar 13, 2026
7487f2a
Fix: send host customization in PACKET_JOIN, add relay logging
zalo Mar 13, 2026
ea4162d
Add relay debug logging for client-to-client visibility
zalo Mar 13, 2026
0fd2ab7
Add printf debug logging for broadcast relay diagnosis
zalo Mar 13, 2026
4474fa7
Fix client-to-client visibility by using server's localIndex for clie…
zalo Mar 13, 2026
2973117
Persist save file to localStorage so progress survives page reload
zalo Mar 13, 2026
b0b0464
Add BackgroundGuard to prevent PeerJS disconnects when tab is backgro…
zalo Mar 13, 2026
01135c3
Fix game loop freezing in background tabs, add client auto-reconnect
zalo Mar 13, 2026
c46bc2c
Add player list overlay with teleport-to-player buttons
zalo Mar 13, 2026
f65d7a1
Replace HTML player list overlay with in-game DJUI display
zalo Mar 13, 2026
a4cf0e2
Revert "Replace HTML player list overlay with in-game DJUI display"
zalo Mar 13, 2026
d78428c
Revert "Add player list overlay with teleport-to-player buttons"
zalo Mar 13, 2026
8459846
Add teleport button to existing Tab player list panel
zalo Mar 13, 2026
ff01c34
Enable mouse cursor when Tab playerlist is visible
zalo Mar 13, 2026
ddf68ff
Fix TP button: cursor cleanup, crash, and cross-level warping
zalo Mar 13, 2026
96e66b0
Make host automatically admin, remove overlay hide delay
zalo Mar 13, 2026
cfa00ac
Add web mod manager with upload, enable/disable, delete, and persistence
zalo Mar 13, 2026
04690b6
Support .zip mod uploads with extraction and full IndexedDB persistence
zalo Mar 13, 2026
d86b87f
Ensure mods and .tmp directories exist on VFS before main()
zalo Mar 13, 2026
c7c3da5
Add touch gamepad overlay for mobile browsers
zalo Mar 13, 2026
68ef035
Keep overlay visible until room is joined when ?room= is in URL
zalo Mar 13, 2026
63074be
Reduce overlay fallback timeout from 15s to 5s
zalo Mar 13, 2026
cec18d4
Split mod upload into separate .lua and .zip buttons for iOS compat
zalo Mar 13, 2026
f1929a3
Add mod URL download box to mod manager overlay
zalo Mar 13, 2026
3937d1c
Remove mod URL download box (CORS makes it unusable)
zalo Mar 13, 2026
6b3843d
Revert "Remove mod URL download box (CORS makes it unusable)"
zalo Mar 13, 2026
56a67a7
Simplify mod URL box to direct .lua/.zip links only
zalo Mar 13, 2026
78d70b7
Auto-convert GitHub blob URLs to raw for mod URL downloads
zalo Mar 13, 2026
c758c5e
Persist mod enabled/disabled state to localStorage
zalo Mar 13, 2026
e1c36b6
Combine Host+Join into single PLAY button with auto-detect, enable mo…
zalo Mar 13, 2026
b496140
Add PeerJS beforeunload cleanup and mobile keyboard for DJUI inputs
zalo Mar 13, 2026
d11a07d
Fix mobile keyboard: use user-gesture-driven focus for iOS/Android
zalo Mar 13, 2026
075b5a6
Simplify PLAY button: set ?room= in URL and reload page
zalo Mar 13, 2026
62ac527
Add SSAO post-processing pass (Phase 1: AO-only, GLSL 1.20)
zalo Mar 13, 2026
94d6519
Fix SSAO shaders for WebGL: use GLSL ES 1.00, fix depth format
zalo Mar 13, 2026
a1a8d87
Fix SSAO WebGL: enable depth texture extension, guard FBO binding
zalo Mar 13, 2026
5a2a231
Fix SSAO for WebGL 2: use GL_DEPTH_COMPONENT24, skip extension check
zalo Mar 13, 2026
78813b3
Fix SSAO depth reconstruction: transpose projection matrix for GLSL
zalo Mar 13, 2026
2091e69
Add SSAO debug thumbnails: depth, linearized depth, normals, AO
zalo Mar 13, 2026
69065f0
Fix SSAO inverse projection: GL_FALSE not GL_TRUE
zalo Mar 13, 2026
8209b5f
Fix SSAO: use hardcoded SM64 camera params instead of N64 P_matrix
zalo Mar 14, 2026
531c5f8
Tune SSAO params, add debug thumbnails, expose tunable globals
zalo Mar 14, 2026
91a9d3b
Add SSAO tuning sliders to Display settings panel
zalo Mar 14, 2026
8ac054a
Fix AO thickness scale for N64 units (×50 instead of ÷10)
zalo Mar 14, 2026
ab6067f
Fix inverted AO thickness check — was keeping far, rejecting close
zalo Mar 14, 2026
7ee6c37
Fix AO: remap 50% baseline to white, skip far samples instead of mix
zalo Mar 14, 2026
8f8f074
Add SSGI integration notes documenting all bugs, gotchas, and hacks
zalo Mar 14, 2026
b14c519
Integrate SSGI response fixes: GTAO formula, game-level capture, UI s…
zalo Mar 14, 2026
d538f56
Disable ortho switch mid-frame composite (caused blank screen)
zalo Mar 14, 2026
09c4208
Revert GTAO formula (produced inverted AO), restore working simple fo…
zalo Mar 14, 2026
718161a
Replace ao*2.0 hack with proper horizon angle clamping
zalo Mar 14, 2026
235c4cb
Restore ao*2.0 hack, document baseline problem for oracle review
zalo Mar 14, 2026
d3052fb
Implement proper GTAO cosine-weighted AO per oracle response
zalo Mar 14, 2026
7658112
Add raw maxHorizonCos debug panels, disable SSAO by default
zalo Mar 14, 2026
7166ef1
Revert AO shader to working 235c4cb version with ao*2.0
zalo Mar 14, 2026
60f429d
Fix mod import crash: stack overflow in mod_load_files_dir on Emscripten
zalo Mar 14, 2026
608bc14
Preload mods/ directory into Emscripten VFS for web builds
zalo Mar 14, 2026
ae1d98e
Web: route Play button through host panel for mod/settings config
zalo Mar 14, 2026
1aafc10
Fix mod refresh button stuck on "Refreshing..." forever on web
zalo Mar 14, 2026
c153df2
Fix black screen on Android: use mobile-safe GL attributes for web
zalo Mar 14, 2026
79189b8
Fix Android black screen: remove emscripten_webgl_commit_frame, allow…
zalo Mar 14, 2026
9eb0ae5
Stop JS mod manager from overriding C config mod enabled state
zalo Mar 14, 2026
823e444
Update README with web port documentation and live deployment link
zalo Mar 14, 2026
ac93fb0
Preload dynos/packs into Emscripten VFS, persist uploaded DynOS packs
zalo Mar 14, 2026
19bbb07
Merge remote-tracking branch 'origin/dev' into feature/emscripten-web…
zalo Mar 14, 2026
ad13edd
Add Cloudflare Durable Object room registry with DJUI lobby browser
zalo Mar 14, 2026
6501650
Point lobby API URLs to deployed Cloudflare Worker
zalo Mar 15, 2026
75c5966
CI: build stable + audio branches in parallel, deploy to separate paths
zalo Mar 15, 2026
2f2bbe1
CI: fix artifact paths in deploy step
zalo Mar 15, 2026
5ae351a
Disable SSGI on web builds and add per-frame profiler
zalo Mar 15, 2026
2ec5e48
Restore native audio code paths for web builds
zalo Mar 16, 2026
d4a8462
Add 32-bit LE sound data for web builds, enabling SFX playback
zalo Mar 17, 2026
031f26e
Fix music sequences on web by adjusting ROM-loaded sequence offsets
zalo Mar 17, 2026
51e43e8
Use structural CTL converter instead of assemble_sound.py regeneration
zalo Mar 17, 2026
6e62fff
Remove separate audio build from CI, update CLAUDE.md
zalo Mar 17, 2026
2b4d823
Re-enable SSGI on web, add star cheat, fix iOS audio
zalo Mar 17, 2026
ef2530e
Enable 4x MSAA antialiasing on web with automatic fallback
zalo Mar 17, 2026
ba851e7
Remove star cheat, quiet PeerJS logs, auto rooms, IndexedDB saves, lo…
zalo Mar 17, 2026
000bc89
Add live lobby dashboard at worker root URL
zalo Mar 17, 2026
31baf01
Fix DJUI lobby list not populating: uint64_t/number ABI mismatch
zalo Mar 17, 2026
b24a9be
Create NewIcon.png
zalo Mar 17, 2026
46cbc39
Add PWA support, iOS fullscreen, and improved touch controls
zalo Mar 17, 2026
7d7037a
Fix DJUI click-through, service worker, auto room IDs, join failure a…
zalo Mar 17, 2026
e18b797
Fix PWA start_url: use ./ instead of sm64coopdx.html
zalo Mar 17, 2026
1552668
Fix home screen access, landscape resize, and auto room ID placement
zalo Mar 17, 2026
3d77bff
Revert touch controls to original, fix canvas sizing
zalo Mar 17, 2026
22f844a
Revert viewport-fit switching, fix touch offsets, document iOS tradeoff
zalo Mar 17, 2026
e27085d
Add middle-click Z bind; fix direct join; revert viewport changes
zalo Mar 17, 2026
a29ab93
Self-host PeerJS signaling server on Cloudflare Durable Object
zalo Mar 18, 2026
089bab4
Fix PeerJS path doubling: set path:'/' so PeerJS appends /peerjs corr…
zalo Mar 18, 2026
6415dae
Revert to e27085d — remove self-hosted PeerJS signaling
zalo Mar 18, 2026
13b7cd6
Fix host state corruption when max players exceeded
zalo Mar 18, 2026
1277ec1
Add comprehensive [Net] connection logging throughout PeerJS flow
zalo Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/workflows/build-web.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Build Web (Emscripten) & Deploy to GitHub Pages

on:
push:
branches: [ main, feature/emscripten-web-port ]
workflow_dispatch:

# Allow only one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true

permissions:
contents: read
pages: write
id-token: write

jobs:
build-stable:
runs-on: ubuntu-22.04
steps:
- name: Checkout stable branch
uses: actions/checkout@v4
with:
ref: feature/emscripten-web-port
submodules: recursive

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential git python3 libglew-dev libsdl2-dev libz-dev zip

- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 3.1.64

- name: Cache Emscripten system libs
uses: actions/cache@v4
with:
path: ~/.emscripten_cache
key: emsdk-3.1.64-${{ runner.os }}

- name: Cache stable build objects
uses: actions/cache@v4
with:
path: build/us_pc
key: build-web-stable-${{ runner.os }}-${{ hashFiles('src/**', 'include/**', 'lib/**', 'Makefile', 'sound/**', 'levels/**', 'actors/**', 'assets/**') }}

- name: Verify Emscripten
run: emcc --version

- name: Clean stale build objects
run: |
find build/us_pc -name '*.o' -delete 2>/dev/null || true
find build/us_pc -name '*.d' -delete 2>/dev/null || true

- name: Build stable for web
run: |
emmake make TARGET_WEB=1 VERSION=us -j$(nproc)

- name: Prepare stable artifact
run: |
mkdir -p _stable/proxy _stable/icons
cp build/us_pc/sm64coopdx.html _stable/
cp build/us_pc/sm64coopdx.js _stable/
cp build/us_pc/sm64coopdx.wasm _stable/
cp build/us_pc/sm64coopdx.data _stable/ 2>/dev/null || true
cp build/us_pc/manifest.json _stable/ 2>/dev/null || true
cp build/us_pc/sw.js _stable/ 2>/dev/null || true
cp build/us_pc/icons/*.png _stable/icons/ 2>/dev/null || true
cp src/pc/web/proxy/proxy.js _stable/proxy/ 2>/dev/null || true
cp src/pc/web/proxy/package.json _stable/proxy/ 2>/dev/null || true
cp src/pc/web/proxy/README.md _stable/proxy/ 2>/dev/null || true

- name: Upload stable build
uses: actions/upload-artifact@v4
with:
name: stable-build
path: _stable/

deploy:
needs: [build-stable]
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Download stable build
uses: actions/download-artifact@v4
with:
name: stable-build
path: artifacts/stable

- name: Assemble Pages site
run: |
mkdir -p _site/proxy _site/icons

# Build at root
cp artifacts/stable/sm64coopdx.html _site/index.html
cp artifacts/stable/sm64coopdx.js _site/
cp artifacts/stable/sm64coopdx.wasm _site/
cp artifacts/stable/sm64coopdx.data _site/ 2>/dev/null || true
cp artifacts/stable/manifest.json _site/ 2>/dev/null || true
cp artifacts/stable/sw.js _site/ 2>/dev/null || true
cp -r artifacts/stable/icons/* _site/icons/ 2>/dev/null || true
cp -r artifacts/stable/proxy/* _site/proxy/ 2>/dev/null || true

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/lua/lua-5.3.6"]
path = lib/lua/lua-5.3.6
url = https://github.com/lua/lua.git
149 changes: 149 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

SM64CoopDX is an online multiplayer mod for Super Mario 64, originally a native C application. The `feature/emscripten-web-port` branch adds a browser-based version using Emscripten/WebAssembly with PeerJS (WebRTC) for peer-to-peer multiplayer networking.

## Build Commands

### Web (Emscripten) Build
```bash
# Prerequisites: Emscripten SDK installed at ~/emsdk or /usr/local/emsdk
./build_web.sh # Incremental build (DEBUG=1, VERSION=us)
./build_web.sh clean # Clean build (removes build/us_pc first)
```
Under the hood: `emmake make TARGET_WEB=1 VERSION=us DEBUG=1 -j$(nproc)`

### Serve Locally
```bash
./serve.sh # Starts HTTP server (port 8083) + WebSocket proxy (port 8765)
# Game at: http://localhost:8083/sm64coopdx.html
```

The HTTP server (`python3 -m http.server`) does NOT set COOP/COEP headers needed for SharedArrayBuffer. Use `src/pc/web/serve.py` if you need those headers.

### Proxy Server (standalone)
```bash
cd src/pc/web/proxy && npm install
node proxy.js --target-host 127.0.0.1 --target-port 7777 --listen-port 8765
```

## Architecture: Web/Emscripten Port

### Platform Abstraction

The codebase uses conditional compilation (`#ifdef TARGET_WEB`) and pluggable backends via function-pointer structs:

- **Network:** `struct NetworkSystem` in `network.h` — on web, points to WebSocket/PeerJS implementation
- **Threading:** `src/pc/thread_web.c` — all no-ops (single-threaded on web)
- **Sockets:** `src/pc/network/socket/socket.h` conditionally includes `socket_websocket.h` instead of platform UDP headers

### PeerJS Networking (the key web-specific system)

**Two layers:**

1. **JavaScript side** (`src/pc/web/shell.html`, lines ~567-747): `PeerNetwork` object
- Manages PeerJS Peer instances, DataConnections, slot ID assignment
- Deterministic host peer ID: `'sm64-' + roomId + '-host'`
- Auto-detection: tries to register as host first; falls back to client on `unavailable-id` error
- Maintains `recvBuffer` array of `{slotId, data}` packets

2. **C side** (`src/pc/network/socket/socket_websocket.c`): EM_JS bridge functions
- `peer_init(roomId)`, `peer_send(slotId, data, len)`, `peer_drain_recv()`, `peer_is_connected()`, `peer_shutdown()`
- Ring buffer `sRecvBuf[128KB]` with packet format: `[u16 slotId LE][u16 length LE][data...]`

**Packet flow:**
```
PeerJS DataConnection → JS recvBuffer → peer_drain_recv() → C sRecvBuf
→ network_update() processes → peer_send() EM_JS → PeerNetwork.send() → DataConnection
```

**Host relay:** The host (whether web or native) relays packets between clients. Broadcast packets (slotId=0) go to all except sender. This is handled in `network.c` (~lines 206-245).

### WebSocket-to-UDP Proxy (`src/pc/web/proxy/proxy.js`)

Alternative transport for browser↔native server communication. Two modes via query params:
- **Client mode** (`?target=HOST:PORT`): 1:1 WebSocket↔UDP bridge
- **Host mode** (`?host=UDPPORT`): multiplexes clients with 2-byte slot prefix

### Game Loop on Web (`src/pc/pc_main.c`)

Uses `requestAnimationFrame` (not `emscripten_set_main_loop`) calling `web_one_iteration()` at 30 Hz. Each frame: handle SDL events → `network_update()` (drain PeerJS packets) → game logic → Lua scripting → `buffer_audio()`.

### Audio on Web

Audio is fully enabled on the web build using SDL2's Emscripten audio backend (ScriptProcessorNode). Sound effects and music sequences both play faithfully.

**The 32-bit pointer problem:** The pre-compressed sound data (`sound/sound_data_compressed.ctl`, `.tbl`, `sequences_compressed.bin`) was generated by `tools/assemble_sound.py` on a 64-bit host. The binary format uses pointer-sized fields (`P` = 8 bytes on 64-bit, 4 bytes on 32-bit) and padding (`X` = 4 bytes on 64-bit, 0 on 32-bit). On WASM (32-bit), the C structs (`ALSeqFile`, `ALSeqData`, `AudioBank`, `Instrument`, `Drum`, etc.) expect 4-byte pointers, causing struct field misalignment when reading 64-bit data.

**Solution:** The `sound/web/` directory contains 32-bit little-endian versions of all sound data files. The Makefile conditionally uses these when `TARGET_WEB=1`.

**How the web sound data was generated:**
- **CTL (audio banks):** `tools/convert_ctl_32.py` structurally walks each bank's 64-bit body, converting P (8→4 byte) and X (4→0 byte) fields while remapping all internal pointer offsets. This preserves exact original bank data (ADSR envelopes, tuning, codebooks). Do NOT use `assemble_sound.py` regeneration — it re-encodes AIFC samples, producing different codebook parameters that cause pitch errors and audio artifacts.
- **TBL (sample data) and sequences:** `tools/convert_seqfile_32.py` converts only the ALSeqFile header (entry table). The data blobs are opaque byte streams with no pointer fields, so they're copied verbatim.
- **Offset headers:** `samples_offsets.h` and `sequences_offsets.h` need delta-adjusted versions in `sound/web/` because the 32-bit ALSeqFile header is smaller, shifting data start positions. The delta is `align16(8 + count*16) - align16(4 + count*8)`.

**Important notes:**
- The sequences file only contains seq 0 (SFX player) inline — all other sequences are loaded from ROM at runtime via `ROM_ASSET_LOAD_SEQUENCE`.
- `samples_assets.c` and `sequences_assets.c` conditionally include from `sound/web/` when `TARGET_WEB` is defined.
- If the original sound data changes (new banks, sequences, etc.), regenerate web versions by running the converter scripts on the updated compressed files.

**Key files:**

| File | Role |
|------|------|
| `sound/web/sound_data_compressed.ctl` | 32-bit LE audio bank control data (45 banks) |
| `sound/web/sound_data_compressed.tbl` | 32-bit LE header + original sample data |
| `sound/web/sequences_compressed.bin` | 32-bit LE header + original sequence data |
| `sound/web/samples_offsets.h` | Sample byte offsets adjusted for 32-bit TBL header |
| `sound/web/sequences_offsets.h` | Sequence byte offsets adjusted for 32-bit SEQ header |
| `tools/convert_ctl_32.py` | Structural CTL converter (banks with P/X fields) |
| `tools/convert_seqfile_32.py` | Header-only converter for TBL and sequences |

### Persistence

- **Config:** `localStorage['sm64coopdx_config']` restored to Emscripten VFS on startup
- **Save files:** `localStorage['sm64coopdx_save']` (base64), persisted via `web_save_savefile()`
- **ROM:** Cached in IndexedDB, validated by Z64 magic number, byte-swapped from N64/V64 formats

### Emscripten Linker Settings

Key flags in Makefile (~lines 941-952):
- `ASYNCIFY` with 65536 stack — allows C to call async JS
- `INITIAL_MEMORY=268435456` (256MB), `ALLOW_MEMORY_GROWTH`
- `FORCE_FILESYSTEM` + `libidbfs.js` for virtual filesystem
- Exported runtime: `ccall`, `cwrap`, `FS`, `allocateUTF8`

### Disabled on Web

Discord SDK, CoopNet (cloud hosting), Mumble voice, update checker, native threading.

### iOS Viewport / Fullscreen Notes

Using `viewport-fit=cover` in the viewport meta tag gives true edge-to-edge fullscreen rendering on iOS (no black bars in portrait or landscape). However, dynamically switching `viewport-fit` between orientations causes touch coordinate offsets on the gamepad overlay buttons — the `position: fixed` elements end up misaligned with where touches register. The current approach uses the default viewport (`width=device-width, initial-scale=1.0`) which has correct touch mapping but leaves a status bar gap in portrait and side bars in landscape on iOS. A future fix could use `viewport-fit=cover` permanently if the touch gamepad coordinates are adjusted to account for `env(safe-area-inset-*)` offsets.

## Key Files

| File | Role |
|------|------|
| `src/pc/web/shell.html` | Emscripten HTML shell + PeerNetwork JS (~857 lines) |
| `src/pc/network/socket/socket_websocket.c` | C↔JS bridge via EM_JS (~360 lines) |
| `src/pc/network/network.c` | Core networking, relay logic |
| `src/pc/network/network.h` | NetworkSystem struct, NetworkType enum |
| `src/pc/pc_main.c` | Game entry, `web_one_iteration()` |
| `src/pc/web/web_main.c` | ROM/config persistence, URL param handling |
| `src/pc/web/proxy/proxy.js` | WebSocket↔UDP proxy server |
| `src/pc/network/packets/packet.c` | Packet processing pipeline (45+ packet types) |

## URL Parameters

The web build supports auto-join/host via URL:
- `?room=NAME` — PeerJS room identifier
- `?join=HOST:PORT` — auto-join a server
- `?host=PORT` — auto-host a server

## CI/CD

`.github/workflows/build-web.yaml` builds on Ubuntu 22.04 with emsdk 3.1.64, deploys to GitHub Pages.
Loading