Syrinx
Completed
Product · Web

Syrinx

Team member — Godot loader & isolation headers · Creative Computing Society · 2024

Browser game portal for the society, whose interesting artefact is the loader: a hand-written React wrapper around the Godot HTML5 engine with its own progress UI, plus the Netlify and Vite rewrites that inject the cross-origin isolation headers a WebAssembly build needs, streaming a 24 MB level pack from a DigitalOcean Spaces CDN. The game itself was somebody else's work. One level shipped, around a shell of cookie-based auth, level select, a leaderboard, a pause menu and a rulebook — and in the deployed build the login and leaderboard calls still point at localhost, so what the Netlify site really demonstrates is the loader.

Built with
ReactReact
ViteVite
Godot 4Godot 4
WebAssemblyWebAssembly
TailwindTailwind
AxiosAxios
NetlifyNetlify
Project Details

STATUS
Completed
ROLE

Team member — Godot loader & isolation headers

ORGANISATION

Creative Computing Society

YEAR

2024

TYPE

Product · Web

TAGS
Game
WebAssembly
Frontend

Syrinx was a Creative Computing Society event built around a browser game rather than a website. The levels are authored in Godot 4 and compiled to WebAssembly, then streamed from a CDN and mounted inside a React app. That combination is the whole engineering problem: a 24.3 MB game pack cannot sit in your JS bundle, and the Godot runtime will not start unless the browser has granted the page cross-origin isolation. A team of us put it together over about a week in July 2024.

Cross-origin isolation is the part that surprises people. Godot's web export uses threads, threads need SharedArrayBuffer, and browsers stopped handing out SharedArrayBuffer after Spectre unless a page proves nothing cross-origin can read its memory. Proving that means serving the document with Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Opener-Policy: same-origin, and then every subresource has to satisfy them too. So the moment you decide to host a 24 MB pack on a CDN to keep it out of your bundle, you have also decided that the CDN is now inside your isolation boundary, and the naive fix - point the engine at the CDN URL - fails with an error message about a missing feature rather than about headers.

Getting the engine to run

The game is not served by the app. netlify.toml rewrites /level-1/* to a DigitalOcean Spaces CDN with status = 200 and force = true, so it proxies rather than redirects and the assets stay same-origin. The same rule injects Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Opener-Policy: same-origin, which is what the browser demands before it will hand Godot a SharedArrayBuffer. The Vite dev server mirrors both headers on its own proxy, so local development fails in the same places production would instead of working locally and breaking on deploy. A _redirects file duplicates the same proxy rule as a fallback, because the two Netlify configuration formats do not always agree about precedence.

src/godot/gdLoader.jsx is a hand-written React wrapper around the Godot HTML5 window.Engine runtime. It drives the engine's loading UI from refs: a determinate progress bar while the pack downloads, and an eight-segment indeterminate spinner stepping every 100 ms while the runtime boots. gdConfig.js declares fileSizes for the 24.3 MB level-1.pck up front, so the bar is accurate from the first byte rather than waiting on a content-length header, and names the service worker the export needs registered. The 3.3 MB engine JS is pulled in from a plain script tag in index.html.

The loader also owns the failure path, which for a 24 MB download over event Wi-Fi is not hypothetical. initGame takes a retry count, defaults it to three, and on a rejected startGame waits a second and calls itself again before finally rendering the engine's error text into the notice pane. What it does not do is run Godot's own getMissingFeatures() preflight, which is commented out - so a browser that never got its isolation headers fails at engine start with a generic message instead of being told which feature is missing.

What the shell provides

  • Cookie sessions: login posts to the API and stores the returned token via js-cookie with secure, sameSite: strict and a one-day expiry, and logging out removes it and returns to the login screen.
  • Keyboard-driven main menu, arrow keys to move the selection and Enter to navigate, with the logo sliding to whichever item is focused. Mouse hover writes to the same focus index, so the two input methods share one piece of state rather than fighting. It reads like a console menu rather than a web nav bar.
  • Level select generated from the game config, so adding a level is a config entry instead of a new route.
  • Live leaderboard reading /leaderboard/{limit}/{offset}, with the top three team names positioned over a podium graphic and the rest in a scrolling rank table.
  • Rulebook rendered inside a scroll illustration, a members page, and a pause-menu overlay reusing the same keyboard-driven menu component with Resume in place of Play Game.
  • Netlify deploy with a _redirects fallback duplicating the CDN proxy rule.

Where it stops short of shipped

The repository is the state of a week, and it does not pretend otherwise. Three things in particular a reader should know before treating it as a finished product.

The route guard is a passthrough. PrivateRoute reads the token cookie and then returns its children unconditionally, with the real check commented out and a TODO at the top of App.jsx to put it back before shipping. The login and leaderboard calls both point at localhost:8080, so the API base URL never made it into configuration. And the level route accepts an :id parameter that the component ignores in favour of a hardcoded index of zero, which is invisible while there is exactly one level and would be the first bug found on the second.

The content is placeholder in the same way: the rulebook holds one sentence repeated, the members page four identical cards, and Pagination.jsx renders an empty div. The whole 63 MB Godot export is also committed into public/ under a directory named efewfe, despite being proxied from the CDN in every environment.

Scope and credit

This was a society project with five people committing to it, and the game itself was somebody else's work. I contributed as part of the team rather than owning the repository. One level shipped: level-1 is the only entry in the config and the only pack in public/. The interesting artefact is the loader plus the isolation headers, which is the piece that took real work to get right and is reusable for any Godot build you want to put inside a React route.

Project Details

STATUS
Completed
ROLE

Team member — Godot loader & isolation headers

ORGANISATION

Creative Computing Society

YEAR

2024

TYPE

Product · Web

TAGS
Game
WebAssembly
Frontend