# 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://git@git.devlabhq.net: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.)