Files
tokyo-wangan-racing/PROJECT-BRIEF.md
T
jesse 25d1e37750
CI / quality (push) Failing after 1m4s
docs: rojo watchdog (RojoTWR-Guard) — rojo does not stay up on its own
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.
2026-08-06 21:59:48 -04:00

15 KiB
Raw Blame History

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:

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 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 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/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:

$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:

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

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.)