397 lines
20 KiB
Markdown
397 lines
20 KiB
Markdown
# Tera — architecture
|
||
|
||
**Tera** is the map view: California and its cities from above, rendered in three.js, public and
|
||
self-hostable. **Spaces** is the other half — the offices you walk into. They
|
||
are one engine and one asset library, seen from outside and from inside.
|
||
|
||
Lives at `tera.lumbridgecorp.com`. Apache 2.0, on the fleet's own Gitea.
|
||
|
||
## The names
|
||
|
||
Getting these straight early matters, because two of them were nearly overloaded
|
||
onto things that already exist.
|
||
|
||
| name | what it is |
|
||
| --- | --- |
|
||
| **Lumbridge Simulate** | the product line these sit under |
|
||
| **Tera** | the map view. Cities — SF, LA, NYC — from above. Public, no login. |
|
||
| **Spaces** | the interiors product. Offices you enter, host, and self-host. |
|
||
| **Office** | one building's interior, belonging to a tenant. You go *into* an office. |
|
||
| **Space** | a room or zone inside an Office — a desk bay, a meeting room, a lounge. |
|
||
|
||
Two things this avoids. `lumbridge-simulate` is already a **Rust crate** in the
|
||
private monorepo — the Governor's memory-admission simulator — and in its
|
||
vocabulary a **"Scene" is a set of models**, not a three.js scene. This engine
|
||
therefore does not take either word for itself: it is Tera, and its three.js
|
||
scene stays an implementation detail behind `createScene()`.
|
||
|
||
One overload survives on purpose: *Spaces* the product versus *a Space* the
|
||
room. It sits at two different levels and matches how people already talk about
|
||
offices, so it is kept rather than invented around.
|
||
|
||
---
|
||
|
||
## 1. Why this is its own repo, and why it is public
|
||
|
||
Tera began as `/live` inside Workie, the private career portal. That is now
|
||
**deleted**, not deprecated — a 3D city engine has no business living in a job
|
||
tracker, and Workie is a private repo besides.
|
||
|
||
**You cannot cleanly Apache-2.0 a subdirectory of a private repo.** The licence
|
||
attaches to a distribution, and a consumer has to be able to clone the thing the
|
||
licence covers. A folder inside `lumbridgecorp-website` or inside
|
||
`lumbridge-runtime` would mean every release is a manual extraction, and every
|
||
extraction is a chance to ship something private by accident. The point of this
|
||
project — a reusable asset library other people can build on and self-host — needs
|
||
a repo that *is* the open-source artefact.
|
||
|
||
So: **`lumbridge-public/tera` on the fleet's own Gitea.** Not
|
||
GitHub. Self-hosting the forge is the same claim the engine makes: the whole
|
||
thing runs on hardware you own, and nothing about it requires somebody else's
|
||
platform to stay alive.
|
||
|
||
### The 2D world comes down
|
||
|
||
The existing Lumbridge world at `world1.lumbridgecorp.com` is a Phaser 2D
|
||
office, and its tileset is exactly what stops it being open-sourceable. It has
|
||
no real users, so it is being retired rather than migrated — Tera and Spaces
|
||
replace it outright, with original assets, which is the whole point.
|
||
|
||
That reframes this repo: it is not a map that happens to sit next to the
|
||
platform, it is the platform's renderer. The city is the outside of the world,
|
||
offices are the inside, and a tenant moves between them.
|
||
|
||
---
|
||
|
||
## 2. Layout
|
||
|
||
```
|
||
lumbridge-simulate/
|
||
├── src/engine/ # renderer. Knows about terrain, blocks, streets, flights.
|
||
│ ├── types.ts # City, District, Hill, Marker, FlightSource
|
||
│ ├── world.ts # projection + cached heightfield, built per city
|
||
│ ├── terrain.ts # relief, coastline, parks
|
||
│ ├── blocks.ts # the built city, on a per-district street lattice
|
||
│ ├── structures.ts # bridges, streets, freeways
|
||
│ ├── markers.ts # pins. Takes Marker[], knows nothing about companies.
|
||
│ ├── flights.ts # FlightSource interface + a simulated implementation
|
||
│ └── scene.ts # lights, sky, camera flights, render loop
|
||
├── src/interiors/ # the inside of the world — floorplans, desks, presence
|
||
├── src/assets/ # the library. Original meshes and materials, Apache 2.0.
|
||
├── src/transport/ # serialisable route packs + renderer-free fixed-step sim
|
||
├── src/cities/ # data packs. Pure geography, no code.
|
||
│ ├── california.ts # state/corridor scale; LA ↔ SF continuity
|
||
│ ├── sf.ts # ~1000 lines of coastline, hills, districts, landmarks
|
||
│ └── socal.ts # LA / OC / Riverside
|
||
├── src/adapters/ # where outside data plugs in
|
||
│ └── workie.ts # Workie API -> Marker[]
|
||
└── src/main.ts # the standalone demo app
|
||
```
|
||
|
||
The split that matters: **`engine` never imports `cities`, and neither imports
|
||
`adapters`.** A city is data handed to the engine. A marker is a `{id, lat, lng,
|
||
label, colorKey}` handed to the engine. The engine has no idea Workie exists,
|
||
which is what makes it publishable, self-hostable by someone with no Lumbridge
|
||
account at all, and free of anyone's private data.
|
||
|
||
`interiors` and `assets` sit beside `engine` rather than inside it: an office
|
||
and a city share the projection, camera, lighting and render loop, and differ
|
||
only in what they put in the scene. That is why `scene.ts` owns the loop and
|
||
knows nothing about terrain specifically.
|
||
|
||
`transport` also stays outside the renderer. A `TransportPack` is plain JSON and
|
||
`VehicleSimulation` returns plain geographic poses. Three.js enters only in the
|
||
`roadTraffic` render layer, which projects poses through the active `World`.
|
||
That separation lets a future authoritative session server replay the same
|
||
route simulation without importing a browser or a GPU.
|
||
|
||
### 2.1 Product/control-plane boundary
|
||
|
||
Tera/Spaces is the authoritative renderer and simulation client. The separate
|
||
`lumbridgecorp` service remains the control plane: identity, tenant registry,
|
||
membership, billing, provisioning, and short-lived launch grants. It must not
|
||
grow a second world renderer or own frame-by-frame simulation.
|
||
|
||
The public Tera build remains zero-config and useful without any service. Hosted
|
||
presence, multiplayer, profiles, webcam faces, and media surfaces arrive through
|
||
adapters whose absence cannot stop the city, office, traffic, or sky from
|
||
running. This is the replacement path for the retired Phaser World 1; it is not
|
||
a migration of that renderer.
|
||
|
||
The first hosted adapter is now concrete: the Fastify API owns short-lived,
|
||
in-memory authoritative presence sessions and streams validated deltas over
|
||
authenticated POST/SSE. `src/realtime` remains renderer- and transport-neutral;
|
||
the browser transport, interpolation buffer, procedural peer renderer and small
|
||
status UI are separate adapters. A city or full-depth office opts into remote
|
||
peers explicitly, while an anonymous/public scene does not even allocate their
|
||
assets. Interest changes clear the old coordinate frame before a new one is
|
||
shown. Game-state deltas never carry webcam or screen media; those remain
|
||
separate consent and authorization lifecycles.
|
||
|
||
---
|
||
|
||
## 3. Three rules that keep Apache 2.0 honest
|
||
|
||
Apache 2.0 is a promise that everything in the repo is ours to give away. Three
|
||
things would quietly break that promise, and all three are easy to walk into.
|
||
|
||
### 3.1 No trademarks in the repo
|
||
|
||
Company logos are **trademarks**, not code. Salesforce's logo is not ours to
|
||
relicense no matter how the file got here, and an Apache-2.0 repo containing
|
||
`logos/salesforce.svg` is making a claim it cannot back.
|
||
|
||
The NYC atlas ships 47 of these in `public/logos/`. We do not.
|
||
|
||
- Logos are fetched **at runtime**, client-side, and cached in the browser.
|
||
- The repo carries a fetch script and zero logo files. `public/logos/` is
|
||
`.gitignore`d, and CI fails if anything lands there.
|
||
- `NOTICE` carries the standard "trademarks are the property of their
|
||
respective owners; their use here is nominative" line.
|
||
|
||
### 3.2 No OSM-derived coordinates in the repo
|
||
|
||
This is the subtle one, and it is the reason the geography in `cities/sf.ts` is
|
||
hand-traced rather than imported.
|
||
|
||
OpenStreetMap data — including anything that comes out of Nominatim geocoding —
|
||
is **ODbL**. ODbL is share-alike: a "derivative database" has to be released
|
||
under ODbL too. Committing a table of company lat/lngs geocoded from Nominatim
|
||
into an Apache-2.0 repo mixes an incompatible share-alike obligation into a
|
||
permissive one. That is exactly the kind of thing that makes a repo unusable for
|
||
the people we want using it.
|
||
|
||
So:
|
||
|
||
- **Geography** (coastlines, hills, districts) is traced by hand from scratch.
|
||
Original expression, ours, Apache 2.0. This is already true for SF.
|
||
- **Elevation**, when we want real terrain, comes from **USGS/SRTM**, which is
|
||
US-government public domain. Not OSM.
|
||
- **Geocoded company coordinates never enter this repo.** They live in a private
|
||
database and arrive over the API at runtime.
|
||
|
||
There is a real result here: the licence constraint and the privacy constraint
|
||
want the same thing. Company positions and pipeline status both stay behind the
|
||
API; the open-source repo holds the city and the renderer. The tempting shortcut
|
||
— "just commit an `sf-companies.json`" — breaks both at once.
|
||
|
||
#### Correction: containment is not discharge
|
||
|
||
An earlier version of this section stopped at the paragraph above, and it was
|
||
**wrong** in a way worth recording rather than quietly fixing.
|
||
|
||
Keeping OSM-derived coordinates out of the repo solves licence *mixing inside
|
||
the repo*. It does not touch ODbL's actual trigger. Serving a snapshot of those
|
||
coordinates from a public endpoint is **Publicly Using a Derivative Database**,
|
||
which brings ODbL §4.3 attribution and §4.4 share-alike onto the served data
|
||
**regardless of where the rows are stored**. Moving the table off-disk hides the
|
||
obligation; it does not discharge it.
|
||
|
||
So the rule is about the *geocoder*, not the storage:
|
||
|
||
- The sanctioned geocoder is the **US Census Geocoder**
|
||
(`geocoding.geo.census.gov`) — a US Government work in the public domain,
|
||
keyless, covering every city planned here.
|
||
- **Google, Mapbox and HERE do not solve this either.** Their terms restrict
|
||
storing and redistributing returned coordinates, which is precisely what a
|
||
public snapshot does. "Not OSM" is not the test; "may be redistributed" is.
|
||
- Every synced row carries a **provenance** field, and the public-shape
|
||
assertion rejects any row whose provenance is not on a non-ODbL allowlist. The
|
||
gate that already checks field *names* now also checks where a coordinate
|
||
came from.
|
||
|
||
Nothing had been built against the old reasoning when this was caught, which is
|
||
the only reason it was cheap.
|
||
|
||
### 3.3 The engine takes no position on what a marker means
|
||
|
||
`markers.ts` renders `Marker[]`. A marker has a `colorKey`, not a `status`. The
|
||
mapping from "rejected" to red lives in the *adapter*, in the consuming app.
|
||
|
||
That is what lets the same engine serve the private Workie build (coloured by
|
||
pipeline state), the public Lumbridge demo (coloured by sector), and whatever
|
||
someone else builds from this repo, without any of them being a fork.
|
||
|
||
---
|
||
|
||
## 4. Flights: FlightRadar24 is the wrong source for this
|
||
|
||
Real aircraft over the Bay is a great idea. FR24 specifically is a problem, and
|
||
it is worth being clear about why before any code is written against it.
|
||
|
||
FlightRadar24's terms prohibit scraping and prohibit redistributing their data;
|
||
their commercial API is licensed per-seat and explicitly does not grant
|
||
redistribution. An Apache-2.0 repo shipping an FR24 client is publishing
|
||
instructions for violating a ToS, and any FR24 data that reached the browser
|
||
could not be relicensed onward. Neither is fatal to a private deployment, but
|
||
both are fatal to the open-source story that is the point of this repo.
|
||
|
||
The engine therefore takes a **`FlightSource` interface**, and the repo ships
|
||
only sources it can actually give away:
|
||
|
||
| source | licence | notes |
|
||
| --- | --- | --- |
|
||
| **`SimulatedFlights`** | ours, Apache 2.0 | great-circle tracks on real SFO/OAK/SJC approach and departure paths. **This is what ships first**, and it is genuinely enough — the map wants convincing motion, not a spotter's log. |
|
||
| **`adsb.lol` / `airplanes.live`** | community, open terms | free, no key, real aircraft. The default "real data" adapter. |
|
||
| **OpenSky Network** | free, non-commercial | fine for a demo, awkward for a commercial product page. |
|
||
| **your own receiver** | *no licence at all* | an RTL-SDR + `dump1090` on any fleet box in the Bay produces first-party ADS-B. Nothing to comply with, and it is very on-brand. |
|
||
| FlightRadar24 | commercial, no redistribution | if it is ever wanted, it is a **private adapter in the Lumbridge deployment**, not in this repo. |
|
||
|
||
Recommendation: build `SimulatedFlights` now, `adsb.lol` next, and treat a
|
||
receiver as the eventual real answer.
|
||
|
||
---
|
||
|
||
## 5. LA is not "SF but more data"
|
||
|
||
SF is 0.20° × 0.36°. At the 45 m cells the SF heightfield uses, that is 336k
|
||
lattice points and a 1.0 s build. (The pack has since grown to the whole Bay
|
||
Area: 0.53M points and about 2.3 s.)
|
||
|
||
LA + Orange County + Riverside is roughly 0.5° × 1.9° — about **14× the area**.
|
||
The same approach would be 4.6M lattice points and a mesh nobody can load. This
|
||
is the one place where the engine as written does not simply extend, and it is
|
||
better to know that before writing `cities/la.ts` than after.
|
||
|
||
Two changes make it work, and both are cheap now:
|
||
|
||
1. **Cell size is per-city, not a constant.** LA's basin gets coarse cells;
|
||
nothing is lost, because LA's relief is mountains at the edges rather than
|
||
SF's hills every four blocks.
|
||
2. **Cities declare `focusRegions`** — a handful of boxes (DTLA, Santa Monica,
|
||
Culver, Irvine, Pasadena) rendered at fine resolution, with the basin between
|
||
them coarse. This is the LOD story, and it is a per-city data declaration
|
||
rather than engine machinery.
|
||
|
||
SF gets one focus region covering the whole city and behaves exactly as it does
|
||
now. LA gets six. NYC, later, gets Manhattan plus the inner boroughs.
|
||
|
||
### 5.1 The heightfield is built off the main thread
|
||
|
||
Both numbers above are seconds during which nothing rendered and nothing
|
||
responded, because the build ran on the thread that paints. It runs in a Worker
|
||
now (`src/engine/terrain.worker.ts`), which is why `createScene` is async and
|
||
takes a `Stage` rather than a canvas.
|
||
|
||
Three consequences worth knowing before touching it:
|
||
|
||
- **The city pack has to stay structured-cloneable.** It is posted to the
|
||
worker as data. A `City` that acquires a method, a class instance or a
|
||
closure stops being sendable, and the fix is to remove it rather than to
|
||
JSON round-trip around it — a city pack containing code is the thing §2
|
||
exists to prevent.
|
||
- **The sampled accessors stay synchronous.** `elevationAt`, `groundAt` and
|
||
`isLand` are called in tight loops by `terrain.ts`, `blocks.ts` and
|
||
`minimap.ts`; the asynchrony is confined to *becoming ready*, and sampling
|
||
before then is a documented error rather than a silent zero.
|
||
- **There is a main-thread fallback and it is not optional.** An environment
|
||
without Workers — a `file://` open, a locked-down browser — still has to
|
||
build and render, because "clone it and it works" has no exception clause.
|
||
|
||
The same reasoning put Spaces behind an `await import()`: the office is a large
|
||
slice of the bundle and most visitors never open it, so it is fetched when
|
||
somebody reaches for the door rather than by everybody at boot.
|
||
|
||
---
|
||
|
||
## 6. How Workie feeds it
|
||
|
||
Workie stays the system of record for companies. Tera never gets a database.
|
||
|
||
```
|
||
Workie GET /api/live/markers (private, tailnet) -> status colours
|
||
Workie GET /api/public/markers (public, allowlist) -> sector colours
|
||
|
|
||
v
|
||
adapters/workie.ts -> Marker[]
|
||
|
|
||
v
|
||
engine/markers.ts
|
||
```
|
||
|
||
The public endpoint goes through the **same `export-site.ts` field allowlist**
|
||
that already guards radar.karti.ai and work.karti.ai — the one that is
|
||
fail-closed and aborts a deploy rather than shipping an unknown field. No second
|
||
implementation of that gate. That was the argument for keeping `/live` inside
|
||
Workie originally, and it still holds — but only for the *data path*. The
|
||
renderer left; the gate stayed where it was.
|
||
|
||
Self-hosters get neither endpoint and do not need one: `setMarkers()` takes an
|
||
array, and where it comes from is the deployment's business.
|
||
|
||
`lumbridgecorp.com/live` is a static page, so it calls a small
|
||
`lumbridge-simulate` service behind the existing Caddy `handle /api/*` — the
|
||
same pattern `lumbridge-intake.service` already uses on cloud-2 — for flights
|
||
and for proxying the public marker feed.
|
||
|
||
---
|
||
|
||
## 7. Interiors — and why the asset library is the real product
|
||
|
||
The existing Lumbridge world is a Phaser 4 2D office, and Karti's read is right:
|
||
its tileset is the thing that stops it being open-sourceable. Redrawing that in
|
||
3D with original assets is not a graphics exercise, it is a **licensing
|
||
unlock** — it is what lets the walkable-office half of Lumbridge ship under
|
||
Apache 2.0 at all.
|
||
|
||
That makes `src/assets` the highest-leverage part of this repo over time: an
|
||
original, consistent, Apache-2.0 library of desks, chairs, partitions, screens,
|
||
doors, lighting rigs, floor and wall materials. Built once, used by every
|
||
Lumbridge world and by anyone else who wants one.
|
||
|
||
Interiors reuse the engine's renderer, camera machinery and render loop, and
|
||
swap the city layer for a floorplan layer. The city view and the office view are
|
||
the outside and the inside of one world, and moving between them is a scene swap
|
||
rather than a different application.
|
||
|
||
Two claims that stood here earlier were wrong, and CONTRACT.md carries the
|
||
resolutions:
|
||
|
||
- **They do not share a `THREE.Scene`.** They cannot. San Francisco's `latScale`
|
||
puts one scene unit at ~94 m with 3.6× vertical exaggeration; an office renders
|
||
at 1 unit = 1 m. There are two scenes over one renderer, and the city scene is
|
||
*retained and paused* when you step inside rather than disposed — rebuilding
|
||
its 0.53M-point heightfield costs about two and a half seconds on the way back
|
||
out.
|
||
- **A person at a desk is not a `Marker`.** A marker is geographic — it has a
|
||
lat/lng. Presence binds to a **seat id** and never to a coordinate, which is
|
||
what lets the office geometry be public and open-source while who is sitting
|
||
in `eng-04` stays private data behind an API. Same shape as the marker rule,
|
||
one level in.
|
||
|
||
Reference point: the per-office spatial products in this space (Simile and
|
||
friends) are the shape to aim at. The difference Spaces is going for is that this
|
||
one is Apache 2.0 and self-hostable — you can run your own world on your own
|
||
hardware, which is the same claim `lumbridge-compute` makes about compute.
|
||
|
||
This is the next phase after the city lands, and it is where `src/assets` starts
|
||
earning its keep.
|
||
|
||
---
|
||
|
||
## 8. Order of work
|
||
|
||
1. **Port** SF out of Workie into `engine` + `cities/sf`, parameterised by city;
|
||
delete Workie's `/live`. *(this commit)*
|
||
2. **Markers + simulated flights.** A demo worth showing, with no data pipeline
|
||
and no licence questions. *(this commit)*
|
||
3. **Mount at `lumbridgecorp.com/live`**, with a small `lumbridge-simulate`
|
||
service behind Caddy for flights.
|
||
4. **Interiors + the asset library** — the office view, original assets, and
|
||
World 1 moving off Phaser. This is the one that unlocks the platform.
|
||
5. **Workie adapter**, once its geocoding pipeline lands. Workie gets an API,
|
||
not a renderer.
|
||
6. **`cities/la`** — LA / OC / Riverside. Needs `focusRegions` from §5 first.
|
||
7. **NYC.**
|
||
|
||
## 9. Open questions
|
||
|
||
1. **Package name.** `@lumbridge/simulate` implies an npm publish; consuming
|
||
straight from Gitea is simpler until someone outside asks for a registry.
|
||
2. **Licence for the asset library specifically.** Apache 2.0 covers code
|
||
cleanly; art is sometimes better served by CC0 or CC-BY so it can be reused
|
||
outside software. Worth deciding before the first mesh lands, because
|
||
relicensing art after contributors exist is painful.
|
||
3. **How much of World 1 moves at once.** The Phaser world is live and has real
|
||
tenants; the city view can ship at `/live` well before any office does.
|