Verified by killing rojo and confirming the guard restored it with 34872 owned by 'rojo'. Guard refuses to start if the port is held by anything else (sshd tunnel) rather than masking that bug.
15 KiB
PROJECT-BRIEF.md — What we're building and where everything is
Read this first, before
CLAUDE.md.CLAUDE.mdtells 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_typemust 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:
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. It was observed dying repeatedly (Studio restarts, machine sleep, the one-shot task simply ending). A watchdog handles this now:
%USERPROFILE%\rojo-guard.ps1 idempotent restart script
schtasks /Query /TN "RojoTWR-Guard" runs it every 3 minutes
%USERPROFILE%\rojo-guard.log only logs real events, silent when healthy
The guard is deliberately conservative:
- if
rojoalready 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
sshdtunnel) it refuses to start and logsPORT HIJACKED, rather than masking the bug - if
ServerPackagesis missing it logs that instead of letting Rojo silently exit on startup
Verified by killing rojo and confirming the guard restored it with the 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/rojoreturns 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, andnetstatshows the port owned by sshd rather thanrojo. If port 34872 is owned by anything other than a process literally namedrojo, that is the bug.Diagnostic that settles it in one line:
$l=(netstat -ano | Select-String ":34872.*LISTENING"); (Get-Process -Id (($l[0] -split "\s+")[-1])).ProcessNameExpect
rojo. If you getsshd, 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 is001-original-images/001-1993-Mazda-RX-7/with hyphens. Use the real one.
Copy a file over rather than working blind:
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, plusINDEX.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 geometrydocs/japanese-road-standards.md— real road-geometry standardsdocs/CAR_PHYSICS_REFERENCE.txt,docs/MESH_SWAP.md,docs/ROTARY_SOUND.mdassets/audio/— current stand-in RX-7 loops (idle / rev / whistle)audio-work/— raw recordings from real car meets, for the rotary audio workc1_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
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:
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:
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
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.)