Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
5501a1b
Initial impl
aidanfnv Aug 8, 2026
b1088c1
More overlays
aidanfnv Aug 8, 2026
b833483
Add handbrake and pedal brake->reverse behaviour
aidanfnv Aug 10, 2026
d3a9796
Improve pickup dropoff placements
aidanfnv Aug 10, 2026
a7ebd4e
add global timer and scoreboard
aidanfnv Aug 10, 2026
0e9c964
minor improvements
aidanfnv Aug 10, 2026
e3a67e5
Fix idling speed
aidanfnv Aug 10, 2026
7bd3d3f
Show highscore in HUD, remove pickup bonus time
aidanfnv Aug 10, 2026
6f0b442
Faster acceleration
aidanfnv Aug 10, 2026
90bbbdf
Make pickup/dropoffs more random
aidanfnv Aug 10, 2026
2e98ce0
Fix taxi integration on PhysX baseline
aidanfnv Aug 10, 2026
c790f0f
Keep taxi driveable after collisions
aidanfnv Aug 10, 2026
0715880
Strengthen curb steering and reverse transition
aidanfnv Aug 10, 2026
db07766
Make taxi handling more arcade-like
aidanfnv Aug 10, 2026
bdb73be
Place initial taxi pickup ahead of player
aidanfnv Aug 10, 2026
b601a81
Make braking and handbrake turns arcade-sharp
aidanfnv Aug 10, 2026
4f28d11
Make curb recovery follow arcade steering
aidanfnv Aug 10, 2026
228d1f4
Keep collision physics aligned with world rendering
aidanfnv Aug 10, 2026
3a4fe60
Keep vehicle attitude and steering responsive
aidanfnv Aug 11, 2026
e04c3bc
Reduce traffic and bevel vehicle collision shapes
aidanfnv Aug 11, 2026
3420e47
Synchronize native taxi markers with displayed frames
aidanfnv Aug 11, 2026
2309f93
Make arcade steering responsive and consistent
aidanfnv Aug 11, 2026
330ec29
Unify presented state with authoritative physics frames
aidanfnv Aug 11, 2026
1b31587
Keep physics yaw within world model conditioning
aidanfnv Aug 11, 2026
08e6c17
Support Python 3.10 high-score timestamps
aidanfnv Aug 11, 2026
812b4e8
Keep ego heading on the conditioning trajectory
aidanfnv Aug 11, 2026
23a5a91
Restore progressive keyboard steering
aidanfnv Aug 11, 2026
1a258a2
Vary taxi pickups after the first fare
aidanfnv Aug 11, 2026
844bd89
Cap initial pickup distance
aidanfnv Aug 11, 2026
017a198
Isolate taxi physics from Ludus renderer
aidanfnv Aug 11, 2026
a907865
Move taxi game into crazy_robotaxi package
aidanfnv Aug 11, 2026
a00f2e7
Restore Crazy Robotaxi physics policy
aidanfnv Aug 11, 2026
d4aa95f
Isolate Crazy Robotaxi from Interactive Drive
aidanfnv Aug 11, 2026
6e397dd
Add routed turn guidance to Crazy Robotaxi
aidanfnv Aug 11, 2026
b5ae9f8
Improve Crazy Robotaxi intersection guidance
aidanfnv Aug 12, 2026
4d4786e
Synchronize Crazy Robotaxi BEV frames
aidanfnv Aug 12, 2026
1990c61
Expand Crazy Robotaxi pickup choice
aidanfnv Aug 12, 2026
c34ba3a
Remove Crazy Robotaxi turn guidance
aidanfnv Aug 12, 2026
b2a8e83
Limit visible Crazy Robotaxi pickups
aidanfnv Aug 12, 2026
b40a384
Keep Crazy Robotaxi fares inside map bounds
aidanfnv Aug 12, 2026
8a80675
Align Crazy Robotaxi generated frames with world state
aidanfnv Aug 12, 2026
df257df
Fix Crazy Robotaxi native BEV targets
aidanfnv Aug 12, 2026
aa6ce98
Point pickup compass at nearest target
aidanfnv Aug 12, 2026
c67297a
Prefer longer Crazy Robotaxi fares
aidanfnv Aug 12, 2026
14ad298
Keep taxi targets farther from map edges
aidanfnv Aug 12, 2026
7a58001
Enclose the Crazy Robotaxi play area
aidanfnv Aug 13, 2026
0027c67
Fix Crazy Robotaxi play-area enclosure
aidanfnv Aug 13, 2026
346b85a
Enclose interior road-network boundaries
aidanfnv Aug 13, 2026
11e4a57
Add pedestrians to Crazy Robotaxi pickups
aidanfnv Aug 13, 2026
9a8043d
Place Robotaxi stops along road edges
aidanfnv Aug 13, 2026
ba4412d
Extract Crazy Robotaxi as a standalone app
aidanfnv Aug 14, 2026
be12871
Fix Crazy Robotaxi runner manifest options
aidanfnv Aug 14, 2026
980885c
Add playable live-edit abilities to Crazy Robotaxi (skins + coins)
wenqingw-nv Aug 20, 2026
dd3ea05
Add obstacle and weather live-edit events to Crazy Robotaxi
wenqingw-nv Aug 20, 2026
6211ac0
Improve live-edit coin compositing: aspect-true sizing, contact shado…
wenqingw-nv Aug 21, 2026
c507493
Pad the coin contact shadow past its blur radius
wenqingw-nv Aug 20, 2026
ee8e4af
Make weather a base-world-only ability with a corrector-gain dispatch
wenqingw-nv Aug 20, 2026
5336bb8
Add storm preset, stronger snow prompt, and direct weather select
wenqingw-nv Aug 21, 2026
6f5768b
Port fused drift-corrector mode and per-state dispatch from omnidream…
wenqingw-nv Aug 21, 2026
a13d4c7
Wire the fused per-state drift corrector into the live-edit abilities
wenqingw-nv Aug 21, 2026
3285689
Merge branch 'dev/aidanf/game/crazy-robotaxi' into robotaxi-463-live-…
aidanfnv Aug 21, 2026
01b85f4
Gate live-edit native-DIT rejection on real hooks; add corrector mode…
wenqingw-nv Aug 21, 2026
a8b66f4
Composite live-edit pixels on the GPU when the model frame is CUDA-re…
wenqingw-nv Aug 21, 2026
209f0f7
Fix ruff findings in the live-edit branch files
wenqingw-nv Aug 21, 2026
cf78022
Merge branch 'robotaxi-463-live-edit' of https://github.com/wenqingw-…
wenqingw-nv Aug 21, 2026
fd72207
Add --stream-token gate to the MJPEG streaming presenter
wenqingw-nv Aug 21, 2026
345915e
Add demo_kit: hosted-demo launch scripts and runbook
wenqingw-nv Aug 21, 2026
1b9177a
Add hurricane weather preset (kill-gated vs shipped storm)
wenqingw-nv Aug 21, 2026
95a56c2
Bound the coin ability's per-frame cost at course scale
wenqingw-nv Aug 21, 2026
c8bb653
Add traffic events: multi-clone obstacle bursts
wenqingw-nv Aug 21, 2026
854b507
Land weather then release: unguided holds persist, steady-state 2x -> 1x
wenqingw-nv Aug 22, 2026
3594084
Pre-encode swap prompts at session start; swaps inject cached embeddings
wenqingw-nv Aug 22, 2026
3535f6f
Make obstacle box-axis guidance CUDA-graph safe; drop the graph-free …
wenqingw-nv Aug 22, 2026
fc52b31
Add timed power-up skin mode with HUD countdown
wenqingw-nv Aug 22, 2026
ba86ac8
Add --live-edit-skin-first rotation; document timed skin mode
wenqingw-nv Aug 22, 2026
49bba3a
Add MJPEG drop-to-latest backpressure fix and stream bandwidth knobs
wenqingw-nv Aug 22, 2026
200e185
Fix fp32 base-weight aliasing in TextEditLoRA pre-merge (backport)
wenqingw-nv Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/crazy_robotaxi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,59 @@ crazy-robotaxi --help

Use `--stream-mjpeg HOST:PORT` with either entry point to run the browser HUD
instead of opening a local Vulkan window.

## Live-edit abilities

Flag-gated, off by default (`--live-edit-*`; see `crazy-robotaxi --help`,
group "live edit"):

- **Coins** (`--live-edit-coins`, toggle with `C`): collectible coins
composited along the route. Pixel-only — no model hooks; when the model
frame is CUDA-resident (native window fast path, MJPEG pre-download) the
sprites/HUD chips are blended on the GPU with pre-uploaded textures
(< 1 ms per frame), so coins are perf-neutral and work under
`native_dit_acceleration`.
- **Skins** (`--live-edit-style` + `--live-edit-style-lora`, cycle with
`K`): prompt swaps realized by a pre-merged text-edit LoRA. The window
runs single-branch (no per-step extra forward). All skin/weather prompts
are pre-encoded once at session start, so a swap injects cached
embeddings — the swap-boundary chunk no longer pays the 0.5-1 s text
re-encode. `--live-edit-skin-duration-chunks N` turns skins into timed
power-ups: an activation auto-reverts to the base world after N chunks
(11 ≈ 3 s), the HUD chip counts down the remaining time, and `K` keeps
its cycle semantics during an active power-up — next skin, fresh timer
(the default 0 holds a skin until cycled).
- **Weather** (`--live-edit-weather`, cycle with `V`): guided prompt swaps
with no LoRA, deployed land-then-release. **Transient cost: the landing
window costs ~2x per chunk** (a second forward per denoise step) for
`--live-edit-weather-guidance-chunks` chunks (default 6); the weather
then holds unguided at ~1x — it persists through the KV history and the
swapped text (A/B'd: a 27-chunk unguided hold matches the always-guided
policy). `--live-edit-weather-maintain-interval N` optionally re-opens a
short rebased window (`--live-edit-weather-maintain-chunks`, default 2)
every N chunks; the default 0 never does (measured unnecessary).
- **Obstacle / traffic** (`--live-edit-obstacle`, spawn with `O`): cloned
scene vehicles. `--live-edit-obstacle-count N` turns one key press into a
traffic burst of N distinct crossing/oncoming clones staggered ahead of
the ego (`--live-edit-obstacle-stagger-chunks` apart in time; each
despawns after its own pass). Conditioning-only unless
`--live-edit-obstacle-guide-scale > 0`, which adds a second forward per
step while an event is active. The guidance is CUDA-graph safe (guided
steps replay the captured graph twice with box/no-box conditioning
staged in), so the accelerated pipeline stays on; event chunks cost ~2x
model time, non-event chunks are unchanged.
- **Drift correctors** (`--live-edit-style-corrector`,
`--live-edit-base-corrector`, `--live-edit-weather-corrector`): optional
per-state weight-merged correctors. `--live-edit-corrector-mode off`
disables all of them (no transformer weights are snapshotted or touched)
even when checkpoints are configured; `fused` (default) keeps CUDA graphs
and `compile_network` on; `unfused` is the eager fallback (slow,
graph-free pipeline).

Native-DIT interaction: the prompt-swap abilities (skins, weather) need the
Python transformer forward — `replace_text_embeddings` is not wired for the
native optimized-DiT executor, and the LoRA/corrector weight toggles never
reach the native fp8 weight snapshot — so running them requires
`native_dit_acceleration: disabled` in the world-model manifest. Coins (and
obstacle without guidance) do not touch the model and run at full speed
under native DIT.
132 changes: 127 additions & 5 deletions apps/crazy_robotaxi/crazy_robotaxi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
from crazy_robotaxi.input import (
CrazyRobotaxiKeyboardState,
)
from crazy_robotaxi.live_edit.coin_ability import CoinAbility
from crazy_robotaxi.live_edit.config import LiveEditConfig
from crazy_robotaxi.navigation import NavigationLane
from crazy_robotaxi.passengers import (
build_pickup_passenger_trajectories,
)
Expand All @@ -75,17 +78,25 @@ def __init__(
self,
controller: TaxiGameController,
keyboard: CrazyRobotaxiKeyboardState,
*,
style_ability: Any | None = None,
coin_ability: CoinAbility | None = None,
obstacle_ability: Any | None = None,
) -> None:
self._controller = controller
self._keyboard = keyboard
self._style_ability = style_ability
self._coin_ability = coin_ability
self._obstacle_ability = obstacle_ability

@property
def is_running(self) -> bool:
"""Return whether the game is still accepting simulation chunks."""
return self._controller.is_playing

def process_events(self, state: VehicleState) -> None:
"""Consume a pending high-score name submission."""
"""Drain live-edit key requests and a pending high-score name."""
self._process_live_edit_requests()
submitted_name = self._keyboard.consume_taxi_name_submission()
if submitted_name is None:
return
Expand All @@ -95,18 +106,43 @@ def process_events(self, state: VehicleState) -> None:
logger.warning(f"[crazy-robotaxi] ignored high-score submission: {exc}")
self.publish_boundary(state)

def _process_live_edit_requests(self) -> None:
"""Consume rising-edge skin-cycle / coins-toggle key requests."""
requests = getattr(self._keyboard, "live_edit", None)
if requests is None:
return
if requests.consume_skin_cycle() and self._style_ability is not None:
self._style_ability.request_cycle()
if requests.consume_weather_cycle() and self._style_ability is not None:
self._style_ability.request_weather_cycle()
if requests.consume_obstacle_spawn() and self._obstacle_ability is not None:
self._obstacle_ability.request_spawn()
if requests.consume_coins_toggle() and self._coin_ability is not None:
enabled = self._coin_ability.toggle()
logger.info(f"[live-edit] coins {'on' if enabled else 'off'}")

def advance_frames(
self, trajectory: TrajectoryChunk, frame_interval_s: float
) -> ApplicationChunkUpdate:
"""Advance the game and add passengers synchronized to pickup state."""
snapshots = tuple(self._controller.advance_frames(trajectory, frame_interval_s))
if self._coin_ability is not None:
picked = self._coin_ability.advance_frames(trajectory.vehicle_states)
if picked:
logger.info(
f"[live-edit] coin pickup +{picked} "
f"total={self._coin_ability.collected_count}"
)
passengers = build_pickup_passenger_trajectories(
snapshots, trajectory.timestamps_us
)
obstacles: tuple[Any, ...] = ()
if self._obstacle_ability is not None:
obstacles = self._obstacle_ability.advance_frames(trajectory)
return ApplicationChunkUpdate(
trajectory=replace(
trajectory,
dynamic_actors=(*trajectory.dynamic_actors, *passengers),
dynamic_actors=(*trajectory.dynamic_actors, *passengers, *obstacles),
),
frame_application_states=snapshots,
)
Expand All @@ -124,6 +160,9 @@ def __init__(
config: TaxiGameConfig,
keyboard: CrazyRobotaxiKeyboardState,
presenter_config: Any,
*,
live_edit: LiveEditConfig | None = None,
style_ability: Any | None = None,
) -> None:
self._config = config
self._keyboard = keyboard
Expand All @@ -133,6 +172,14 @@ def __init__(
self._ground_snapper: GroundSnapper | None = None
self._map_bounds: MapBounds | None = None
self._enclosure_segments_world = np.empty((0, 2, 3), dtype=np.float32)
self._live_edit = live_edit or LiveEditConfig()
self._style_ability = style_ability
self._live_edit_presenter: Any | None = None
self._coin_lanes: tuple[NavigationLane, ...] = ()

def attach_live_edit_presenter(self, presenter: Any) -> None:
"""Bind the live-edit presenter so coin abilities reach the pixels."""
self._live_edit_presenter = presenter

def configure_presenter(self, presenter: Any) -> None:
"""Configure application presentation before scene loading."""
Expand All @@ -148,6 +195,16 @@ def load_scene(self, scene: SceneBundle, map_bounds: MapBounds | None) -> None:
self._enclosure_segments_world = scene_data.enclosure_segments_world
self._ground_snapper = _build_taxi_ground_snapper(scene)
self._map_bounds = map_bounds
if self._live_edit.coins.enabled:
# Lay coins along the driving-lane graph; legacy scenes without
# mapped lanes fall back to the recorded ego route.
self._coin_lanes = scene_data.navigation_lanes or (
NavigationLane(
centerline_world=np.asarray(
scene_data.reference_route_world, dtype=np.float32
)
),
)
logger.info(
"[crazy-robotaxi] play-area enclosure: perimeter_segments={}",
len(scene_data.perimeter_segments_world),
Expand Down Expand Up @@ -203,7 +260,33 @@ def create_runtime(
initial_camera=scene.selected_camera,
map_bounds=self._map_bounds,
)
return CrazyRobotaxiRuntime(controller, self._keyboard)
coin_ability: CoinAbility | None = None
if self._live_edit.coins.enabled and self._coin_lanes:
# Rebuilt per rollout so a reset restores the full course.
coin_ability = CoinAbility.from_lanes(
self._coin_lanes, self._live_edit.coins
)
logger.info(
f"[live-edit] coin course laid out: {coin_ability.remaining_count} coins"
)
obstacle_ability: Any | None = None
if self._live_edit.obstacle.enabled:
from crazy_robotaxi.live_edit.obstacle_ability import ObstacleAbility

# Rebuilt per rollout so events/hits reset with the game.
obstacle_ability = ObstacleAbility.from_scene(
scene, self._live_edit.obstacle
)
if self._live_edit_presenter is not None:
self._live_edit_presenter.set_coin_ability(coin_ability)
self._live_edit_presenter.set_obstacle_ability(obstacle_ability)
return CrazyRobotaxiRuntime(
controller,
self._keyboard,
style_ability=self._style_ability,
coin_ability=coin_ability,
obstacle_ability=obstacle_ability,
)


class CrazyRobotaxiApp(InteractiveDriveApp):
Expand All @@ -219,8 +302,30 @@ def __init__(
alignment_diagnostics_root: Path | None = None,
trace_sink: TraceSink | None = None,
close_presenter_on_exit: bool = True,
live_edit_config: LiveEditConfig | None = None,
) -> None:
keyboard = CrazyRobotaxiKeyboardState()
live_edit = live_edit_config or LiveEditConfig()
style_ability = None
if live_edit.obstacle.enabled and live_edit.obstacle.guide_scale > 0.0:
from crazy_robotaxi.live_edit.obstacle_ability import (
install_obstacle_guidance_on_backend,
)

# Before the style install so both share one CUDA-graph-free
# session swap (marker-coordinated) and one warmup wrap each.
install_obstacle_guidance_on_backend(backend, live_edit.obstacle)
if live_edit.style.enabled or live_edit.weather.enabled:
from crazy_robotaxi.live_edit.style_ability import (
StyleAbility,
install_style_ability_on_backend,
)

# Must run before super().__init__ starts model warmup on the
# pipeline worker: the corrector needs a CUDA-graph-free session
# and the LoRA attach is deferred to the end of warmup_model.
style_ability = StyleAbility(live_edit.style, live_edit.weather)
install_style_ability_on_backend(backend, style_ability)
if alignment_diagnostics_root is not None:
from crazy_robotaxi.alignment_diagnostics import (
AlignmentDiagnosticPresenter,
Expand All @@ -237,16 +342,33 @@ def __init__(
logger.info(
f"[crazy-robotaxi] alignment diagnostics -> {presenter.output_dir}"
)
application = CrazyRobotaxiApplication(
taxi_config,
keyboard,
config.bev,
live_edit=live_edit,
style_ability=style_ability,
)
super().__init__(
config=config,
backend=backend,
presenter=presenter,
trace_sink=trace_sink,
close_presenter_on_exit=close_presenter_on_exit,
keyboard=keyboard,
application=CrazyRobotaxiApplication(taxi_config, keyboard, config.bev),
application=application,
)
self._presenter = CausalFrameAlignmentPresenter(self._presenter)
inner_presenter = self._presenter
if live_edit.any_enabled:
from crazy_robotaxi.live_edit.presenter import LiveEditPresenter

# Inside the alignment wrapper so composites see the
# frame-synchronized rig_to_world matching the generated RGB.
inner_presenter = LiveEditPresenter(
inner_presenter, live_edit, style_ability=style_ability
)
application.attach_live_edit_presenter(inner_presenter)
self._presenter = CausalFrameAlignmentPresenter(inner_presenter)


def taxi_config_from_args(args: argparse.Namespace) -> TaxiGameConfig:
Expand Down
22 changes: 22 additions & 0 deletions apps/crazy_robotaxi/crazy_robotaxi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,23 @@ def _run_streaming(args: argparse.Namespace) -> None:
parse_bind,
)

stream_token = (getattr(args, "stream_token", None) or "").strip() or None
if stream_token is not None and not getattr(args, "taxi_game", False):
# The upstream engine presenter has no token gate; failing loudly
# beats silently serving an open stream the user believes is gated.
raise SystemExit(
"--stream-token requires the taxi-game streaming presenter; "
"the upstream engine presenter would serve an ungated stream."
)
stream_jpeg_quality = int(getattr(args, "stream_jpeg_quality", 85))
stream_scale = float(getattr(args, "stream_scale", 1.0))
if stream_scale != 1.0 and not getattr(args, "taxi_game", False):
# The upstream engine presenter has no scale knob; failing loudly
# beats silently streaming full-size frames the user asked to shrink.
raise SystemExit(
"--stream-scale requires the taxi-game streaming presenter."
)

_apply_cuda_visible_devices_inplace(args.cuda_visible_devices)
_resolve_demo_paths(args)
_materialize_synthetic_scene_for_picker(args)
Expand Down Expand Up @@ -982,8 +999,11 @@ def _run_streaming(args: argparse.Namespace) -> None:
keyboard=placeholder_keyboard,
bind_host=bind_host,
bind_port=bind_port,
jpeg_quality=stream_jpeg_quality,
scenes=scenes_payload,
thumbnails=thumbnails,
**({"stream_scale": stream_scale} if stream_scale != 1.0 else {}),
**({"stream_token": stream_token} if stream_token is not None else {}),
)

# Build the backend + engine once so the model warms up (on the
Expand Down Expand Up @@ -1087,6 +1107,7 @@ def _build_application(
CrazyRobotaxiApp,
taxi_config_from_args,
)
from crazy_robotaxi.live_edit.config import live_edit_config_from_args

return CrazyRobotaxiApp(
config=config,
Expand All @@ -1097,6 +1118,7 @@ def _build_application(
args, "taxi_alignment_diagnostics", None
),
close_presenter_on_exit=close_presenter_on_exit,
live_edit_config=live_edit_config_from_args(args),
)
return InteractiveDriveApp(
config=config,
Expand Down
26 changes: 26 additions & 0 deletions apps/crazy_robotaxi/crazy_robotaxi/hud_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2842,6 +2842,10 @@ def _build_key_codes(self) -> dict[str, Any]:
"d": _lookup_key(spy.KeyCode, "d"),
"r": _lookup_key(spy.KeyCode, "r"),
"x": _lookup_key(spy.KeyCode, "x"),
"k": _lookup_key(spy.KeyCode, "k"),
"c": _lookup_key(spy.KeyCode, "c"),
"v": _lookup_key(spy.KeyCode, "v"),
"o": _lookup_key(spy.KeyCode, "o"),
"space": _lookup_key(spy.KeyCode, "space"),
"up": _lookup_key(spy.KeyCode, "up", "arrow_up"),
"down": _lookup_key(spy.KeyCode, "down", "arrow_down"),
Expand Down Expand Up @@ -2964,6 +2968,28 @@ def _on_keyboard_event(self, event: Any) -> None:
self._keyboard.request_reset()
elif self._key_matches(key, "x"):
self.exit_scene()
elif self._key_matches(key, "k"):
self._request_live_edit("skin")
elif self._key_matches(key, "c"):
self._request_live_edit("coins")
elif self._key_matches(key, "v"):
self._request_live_edit("weather")
elif self._key_matches(key, "o"):
self._request_live_edit("obstacle")

def _request_live_edit(self, ability: str) -> None:
"""Raise a live-edit key request; no-op when the flags are off."""
requests = getattr(self._keyboard, "live_edit", None)
if requests is None:
return
if ability == "skin":
requests.request_skin_cycle()
elif ability == "weather":
requests.request_weather_cycle()
elif ability == "obstacle":
requests.request_obstacle_spawn()
else:
requests.request_coins_toggle()

def _expire_pending_drive_releases(self) -> None:
"""Commit any debounced release whose grace window has passed.
Expand Down
4 changes: 4 additions & 0 deletions apps/crazy_robotaxi/crazy_robotaxi/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from crazy_robotaxi.high_scores import (
validate_player_name,
)
from crazy_robotaxi.live_edit.input_hooks import LiveEditRequests
from flashdreams.serving.realtime.input import normalize_key


Expand All @@ -44,6 +45,9 @@ def __init__(self) -> None:
self._name_submission: str | None = None
self._keyboard_steer = 0.0
self._last_command_s = time.monotonic()
# One-shot live-edit key requests (skin cycle / coins toggle) raised
# by the presenters and drained by ``CrazyRobotaxiRuntime`` per tick.
self.live_edit = LiveEditRequests()

def submit_taxi_name(self, name: str) -> bool:
"""Validate and queue one high-score name submission."""
Expand Down
Loading