@@ -0,0 +1,357 @@
# PROJECT-BRIEF.md — What we're building and where everything is
> **Read this first, before `CLAUDE.md`.**
> `CLAUDE.md` tells you *how to operate the tools safely*.
> This file tells you *what the game is, why it's built this way, and where the
> resources live*. You need both.
>
> Verified against the live repo, the NAS, and Gitea on **2026-08-06**. Every
> path and number below was checked, not recalled.
---
## 1. The game in one paragraph
**Tokyo Wangan Racing ** is a Roblox night-driving game set on a neon Tokyo
expressway, built around a real 1993 Mazda RX-7 the owner actually owns. It is
modelled on **Tokyo Xtreme Racer ** , not on lap-racing games: **Daikoku Futo PA
(the parking area) is the hub.** You park, choose how you want to run, and drive
out onto the Wangan. You never "join a match."
The core bet: **driving feel is the whole game. ** Racing, economy, and
progression are all deliberately deferred until the car is genuinely fun to just
drive around. Do not add game layers on top of a car that doesn't feel good yet.
### The three modes
Selected in the PA, armed when you cross the exit gate, ended when you pull back
into the PA. Defined in `src/shared/Modes.luau` .
| Mode | 日本語 | What it is | Challenges? | Timed? |
|---|---|---|---|---|
| **CRUISE ** | 流し | Free roam, bank kilometres, meet drivers | yes | no |
| **DUEL ** | バトル | Open a gap and * hold * it — who breaks first | yes | no |
| **TIME ATTACK ** | タイムアタック | Solo flying lap, sector splits, personal best | **no ** | yes |
Cruise and Duel both allow challenges, so a cruise turns into a battle
organically. **Time Attack opts out on purpose ** so a hot lap is never ruined —
that's a design decision, not an oversight. Don't "fix" it.
### The circuit
Closed loop, * * ~49,400 studs (13.8 km)**, samples evenly spaced 60 studs apart so
the loop closes seamlessly. Roughly a 2:46 lap at 300 km/h. Scale is
**1 stud = 0.28 m ** . Coordinates: `+X = east` , `+Z = north` .
Landmark zones (themed, **not ** geographically accurate): Daikoku Futo PA,
Rainbow Bridge, Odaiba (Unicorn Gundam + ferris wheel), Yamate-style tunnel,
Tokyo Tower replica, Tatsumi PA, bilingual green signage (大黒PA, レインボーブリッジ,
お台場, 辰巳PA).
---
## 2. Where you are in the plan
Source of truth: `docs/ROADMAP.md` and `docs/GDD.md` . Current position:
- **M0 — drive-only MVP: DONE.** Spawn, drive, switch cars, respawn, persistence.
- **M1 — make driving feel GOOD: IN PROGRESS. This is the current work.**
- done: constraint chassis (springs, prismatic suspension, servo steering,
motor wheels, engine braking), chase camera, two-layer engine sound
- **not done: import the real RX-7 mesh** (`docs/MESH_SWAP.md` )
- **not done: real 13B rotary audio** from the owner's actual car
(`docs/ROTARY_SOUND.md` ) — current loops are stand-ins
- **not done: tune accel curve + grip** — needs a Studio playtest
- **M2 — more cars + map polish:** map v3 and ambience done; more meshes pending.
- **M3+ — racing, economy, hardening, traffic AI:** deliberately not started.
> **M1's exit test:** *the RX-7 is genuinely fun to just drive around.*
> Ship nothing else until that's true. If asked to build a racing/economy
> feature, flag that it's M3/M4 and confirm before starting.
---
## 3. Repo layout and architecture
Repo: `~/roblox/tokyo-wangan-racing` (Git Bash path on this box).
**Server-authoritative: clients request, the server decides. ** The RX-7
allowlist and anti-cheat are server-side. Keep the client dumb.
```
ReplicatedStorage/Shared src/shared GameConfig, Cars, Circuit, Modes,
Chassis, Remotes, ProfileTemplate
ServerScriptService/Server src/server init.server + managers
ServerScriptService/ServerPackages ServerPackages ProfileStore (Wally)
StarterPlayerScripts/Client src/client HUD, car picker, controllers
```
Mapping is declared in `default.project.json` (Rojo).
| Module | Responsibility |
|---|---|
| `src/server/MapBuilder.luau` | **3,479 lines — the big one. ** Procedurally builds the entire expressway, PA, ramps, barriers, signage. |
| `src/server/CarManager.luau` | Spawns the equipped car; enforces the RX-7 allowlist |
| `src/server/ModeManager.luau` | Mode selection, exit-gate arming, mode end |
| `src/server/DataManager.luau` | ProfileStore wrapper — session lock, reconcile, Studio mock |
| `src/server/AntiCheat.luau` | Speed/teleport sanity check — a speed bump, not a fortress |
| `src/server/Ambience.luau` | Atmosphere haze, bloom, 22:00→03:00 clock drift |
| `src/shared/Circuit.luau` | The 1,050-line circuit spline — sample positions, PA geometry |
| `src/shared/Chassis.luau` | Shared chassis tuning constants |
| `src/client/ChassisController.luau` | Drives the chassis client-side (driver owns network physics) |
| `src/client/CameraController.luau` | Damped chase cam, flat horizon, speed FOV kick |
| `src/client/SoundController.luau` | Two-layer engine crossfade + tyre squeal |
**Cars: ** RX-7 (restricted), City Runabout (public default), Silvia S15, Skyline
GT-R R34, stylized hypercar. The **RX-7 is demo-locked ** to the owner + his son
via `GameConfig.RX7AllowedUserIds` , enforced in `CarManager` and the `EquipCar`
remote. Stats only affect feel — no prices, no unlocks in the MVP.
---
## 4. Resources you can reach — and how
### 4a. Live Roblox Studio (this machine, local)
Two channels, different jobs. **Use [A] to verify what [B] did. **
```
[A] MCP bridge -> READ run Luau in the datamodel, screenshot the viewport
[B] Rojo -> WRITE push .luau source into Studio (~1 s)
```
- `mcp__roblox_studio__execute_luau` — `datamodel_type` **must be `"Edit"` ** ,
never `"Server"` (Server edits a throwaway Play copy that dies on stop).
- `mcp__roblox_studio__screen_capture` — Studio's **3D viewport only ** . Not the
desktop, not Explorer, not Blender.
- Rojo: `rojo serve default.project.json --port 34872` , then **somebody must
click Rojo → Connect in Studio.** It never auto-connects, and an ESTABLISHED
socket on 34872 is idle heartbeat, * not * a sync session.
Full operational detail, version pins, and the failure decoder: * * `CLAUDE.md` **.
#### Rojo must run natively on Windows — never over an SSH tunnel
**Rojo runs on the Windows box, in the Windows checkout. ** Studio, Rojo and the
repo it serves all live on the same machine. Start it with:
``` cmd
cd /d %USERPROFILE% \roblox\tokyo-wangan-racing
%USERPROFILE% \.local\bin\rojo.exe serve default.project.json --port 34872
```
A scheduled task `RojoTWR` does exactly this — `schtasks /Run /TN "RojoTWR"` .
(Launching via `start` over SSH does **not ** work: the process dies when the SSH
session exits. Use the task.)
**Rojo does not stay up on its own. ** The root cause was found on 2026-08-06:
**Windows OpenSSH kills the entire process tree when the SSH session exits. **
Anything started over SSH with `Start-Process` — including Rojo — dies seconds
later. This presents as "Rojo was running a minute ago and now the plugin won't
connect," and it is * not * a plugin fault.
Proof: `rojo procs: 1` inside the SSH session, `rojo procs: 0` from a new
session moments later.
The fix is to hand ownership to **Task Scheduler ** , which is not part of the SSH
process tree:
```
schtasks /Run /TN "RojoTWR" start rojo detached (survives SSH exit)
%USERPROFILE%\rojo-guard.ps1 idempotent watchdog
schtasks /Query /TN "RojoTWR-Guard" runs the watchdog every 3 minutes
%USERPROFILE%\rojo-guard.log only logs real events, silent when healthy
```
Repo copy of the watchdog: `tools/win/rojo-guard.ps1` .
> **Never start Rojo with `Start-Process`/`start` over SSH.** Always go through
> `schtasks /Run /TN "RojoTWR"`. The guard itself follows this rule internally.
The guard is deliberately conservative:
- if `rojo` already owns 34872 it exits 0 and does nothing (safe mid-session —
it will not disturb a live Studio connection)
- if the port is held by **anything else ** (e.g. an `sshd` tunnel) it refuses to
start and logs `PORT HIJACKED` , rather than masking the bug
- if `ServerPackages` is missing it logs that instead of letting Rojo silently
exit on startup
Verified end-to-end: killed `rojo` , waited, and the scheduler restored it
unattended (`Last Result: 0` , port owned by `rojo` ).
**Clicking Rojo → Connect in Studio is still manual and always will be. ** The
guard keeps the * server * alive; it cannot press the button. `Rojo_autoReconnect`
is `false` in the plugin settings, so after a drop you must reconnect by hand
(or flip that setting in the plugin).
> ⚠️ **Do not reverse-tunnel a Linux Rojo to port 34872.** It was tried
> (`ssh -N -R 34872:127.0.0.1:34872`) and it is actively harmful: the port
> answers, `/api/rojo` returns valid JSON, and everything *looks* healthy — but
> it is serving the **Linux** working tree. Studio then syncs files that are not
> the ones being edited on Windows, and `netstat` shows the port owned by
> **sshd** rather than `rojo`. If port 34872 is owned by anything other than a
> process literally named `rojo`, that is the bug.
>
> Diagnostic that settles it in one line:
> ```powershell
> $l=(netstat -ano | Select-String ":34872.*LISTENING"); (Get-Process -Id (($l[0] -split "\s+")[-1])).ProcessName
> ```
> Expect `rojo`. If you get `sshd`, kill the tunnel.
#### `ServerPackages` is gitignored — Rojo will not start without it
`/Packages/` , `/ServerPackages/` and `wally.lock` are **gitignored ** , so a fresh
clone has no dependencies and Rojo **refuses to start ** , exiting immediately:
```
[ERROR rojo] Rojo project referred to a file using $path that could not be
turned into a Roblox Instance by Rojo. File $path: ServerPackages
```
That error means * missing Wally deps * , not a broken project file. There is only
one dependency (`ProfileStore` , 19 files / 316 KB). Fix with `wally install` , or
copy the folder from a machine that already has it. **Rojo exiting on startup is
silent from Studio's side — it just looks like Rojo "won't connect."**
### 4b. Reference material on the NAS (1.5 GB, verified present)
Host `unraid-1` (10.10.10.43), reachable over SSH. Base path:
```
/mnt/user/data/yy-roblox-games/0-tokyo-wangan-racing/
```
| Folder | Contents (verified counts) |
|---|---|
| `001-original-images/001-1993-Mazda-RX-7/` | **12 files ** — the real car, for the mesh swap |
| `001-original-images/002-1993-Nissan-Skyline-GT-R/` | **38 files ** |
| `003-google-earth-images/` | **12 files ** — `daikoku-futo-01..12.png` , aerial geometry |
| `004-video-reference/` | `aerial/` , `contact-sheets/` , `docs/` , `entrance-routes/` |
| `002-completed-models/` | empty — finished models land here |
> ⚠️ **The README currently prints a stale path** for the RX-7 reference
> (`001-1993 Mazda RX-7/` with spaces, at the top level). The real path is
> `001-original-images/001-1993-Mazda-RX-7/` with hyphens. Use the real one.
Copy a file over rather than working blind:
``` bash
scp unraid-1:'/mnt/user/data/yy-roblox-games/0-tokyo-wangan-racing/003-google-earth-images/daikoku-futo-01.png' /tmp/
```
### 4c. Reference material committed in-repo
- `reference/ramps/` — 12 integrity-checked photos of Daikoku/Oi/Heiwajima/
Shibaura PAs, plus `INDEX.md` , `BUILD_SPEC.md` , and OSM/satellite scripts.
**All CC BY / CC BY-SA — reference-only for shape study. Do not trace directly
into shipped assets without attribution.**
- `docs/daikoku-reference-spec.md` — measured spec for the PA entrance geometry
- `docs/japanese-road-standards.md` — real road-geometry standards
- `docs/CAR_PHYSICS_REFERENCE.txt` , `docs/MESH_SWAP.md` , `docs/ROTARY_SOUND.md`
- `assets/audio/` — current stand-in RX-7 loops (idle / rev / whistle)
- `audio-work/` — raw recordings from real car meets, for the rotary audio work
- `c1_georeference.json` — georeferencing data behind the circuit
### 4d. Git / CI
Remote: `ssh://[email protected] :2222/jesse/tokyo-wangan-racing.git`
(self-hosted Gitea on UNRAID-1, SSH key auth already working from this box).
CI runs on a **self-hosted Gitea Actions runner ** (`.gitea/workflows/ci.yml` ),
Ubuntu 24.04 job container — rojo's binary needs glibc 2.39.
### 4e. Existing tooling — check before writing new probes
`tools/` already holds **109 files, 58 of them diagnostic/verification scripts **
(`diag_*.luau` , `verify_*.luau` , `capture_*.py` , `check_*.luau` ). There is a
very high chance a probe already exists for what
you need — `verify_auxlane.luau` , `verify_shared_ramps.luau` ,
`check_live_source.luau` , `scripts/verify_map.luau` , and capture helpers for
flyovers/ramps/defects. **Look there first. **
---
## 5. Quality gates — CI enforces all three
``` bash
stylua --check src/ # formatting
selene src/ # lint
rojo build --output build.rbxl # must compile
```
Conventions:
- Tunables go in `GameConfig` , not scattered magic numbers.
- Add a save key by adding it to `ProfileTemplate` (auto-reconciles). Never
remove a key without a migration.
- Test in Studio after every server change (ProfileStore uses a mock in Studio).
---
## 6. The things that will bite you
These are compressed from hard-won experience. The long versions live in
`CLAUDE.md` — but these three matter most:
### Never regenerate hand-deleted work
**The single most important rule on this project.** If the user deleted
something in Studio, the * source * must honor it or the next rebuild silently
resurrects it — and they will catch it immediately.
Live example, currently in `MapBuilder.luau` :
``` lua
AUX_SUPPRESS_FROM = 525 -- user deleted this aux-lane stretch
AUX_SUPPRESS_TO = 550
AUX_BLEND = 5 -- smoothstep; a hard cut caused a 26-stud fence jog
```
**Do not remove that suppression. ** Hand-placed assets live in
`workspace.PAProps` , which the builder never touches. Before any rebuild, ask:
* does this regenerate something removed by hand? *
### Measure before reasoning, and re-measure after
Write a **read-only probe ** , get real numbers, * then * form a theory. A code
change is not proof — verify against the bug's signature. Enumerate names, not
just totals: a raw count hides a partial removal.
### Beat the `require()` cache
After a Rojo sync the datamodel still holds **old bytecode ** — no error, no
warning, and you will measure the previous build and believe it:
``` lua
local tmp = game.ServerScriptService . Server.MapBuilder : Clone ( )
tmp.Parent = game.ServerScriptService
local MapBuilder = require ( tmp ) -- fresh bytecode, guaranteed
```
### Verified-good baseline (2026-08-06) — regression-check against these
| metric | value |
|---|---|
| total parts | 16,788 |
| PAWestSlip | 60 segs, 34.4° total bend, worst 1.4°, 0 kinks >5° |
| PAEastSlip | 60 segs, 30.7° total bend, worst 1.3°, 0 kinks >5° |
| AuxLane parts | 30 — **samples 525– 550 deliberately absent (user-deleted) ** |
| barrier coverage | 823 samples, 0 unguarded |
| fence jogs >10 studs | 0 |
### Never fabricate
If Studio is closed, say "Studio is closed." Never invent a screenshot, a part
count, or a measurement you didn't obtain. `{'error': '', 'isError': True}` on
every MCP call means **Studio is closed ** — not that your script is broken.
Reporting a blocker honestly always beats a plausible-looking fake.
---
## 7. First five minutes in a new session
``` bash
tasklist | findstr /I "Roblox" # 1. is Studio even running?
claude mcp list # 2. bridge registered + connected?
curl -s http://127.0.0.1:34872/api/rojo # 3. rojo serving?
cd ~/roblox/tokyo-wangan-racing && git status --short && git log --oneline -5
```
Then read `docs/ROADMAP.md` to confirm what milestone is active, and check
`tools/` for an existing probe before writing a new one.
**Working state as of 2026-08-06: ** clean tree, 0 uncommitted files, HEAD at
`dc0df91` . (An older note in `START-HERE.md` warns about "36 uncommitted files"
on the Linux box — that has since been committed and pushed. Trust `git status`
over that note.)