Skip to content

Show a map's doors in replays - #471

Merged
SunkenInTime merged 4 commits into
mainfrom
t3/doors-replay
Nov 2, 2026
Merged

SunkenInTime merged 4 commits into
mainfrom
t3/doors-replay

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Lotus's A door at rest with a cone stopping at it, turning in a spin, side-on, and closed again, in the replay viewer

The A door turning during playback at 1x

ci: test native-replay windows

Fifth and last PR for Lotus and Summit doors. The editor has them since #467 and #468, but a replay showed every door at rest. So a Summit wall dropped in the match stayed up in the viewer, and Lotus cones ran through doors that were open. This reads what the doors did from the replay and shows it.

What a user sees

  • In the viewer, doors move as they did in the match. A Lotus door turns over its 10 s spin and closes again. A Summit wall drops over 1.75 s and stays down for the round. The A Link wall breaks and stays broken. Every door resets when the next round starts.
  • Cones cut against the doors as they are at that moment, on the cone worker as before. A cone only knows a door's two poses, so a spinning door blocks as closed for the first and last quarter of its turn and side-on in between.
  • Capture saves a door row for each door changed at that moment, so the page opens in the editor with the doors as they stood. A door caught moving saves as changed, because a saved door has two states.

Summit's B wall up, then dropped, in the replay

How it reads them

The decoder (native/replay, 0.3.2 to 0.3.3, which re-decodes cached replays) adds a doors list: { actor, startMs, endMs } (docs/replay-format.md, "Doors").

  • The door fields have no names and a patch could renumber them, so none is read. A move is told from when the door's own actor is written: a burst of several writes, away from a round's start, then a last lone write within 12 s and before the next burst. A recording that ends within 12 s of a burst ends the move there. A spin started as a round ends is cut short by the next round's reset, which ends the move. That is where the design's 4 and 5 s spins came from.
  • A Link is the named OnDie on RespawningWallPlate, once a round. Abyss has a wall of the same class, so RespawningWallPlate rows can appear there; no door on Abyss stands for it, so the viewer ignores them.
  • Unnamed writes never reached the analysis, so the collector keeps door groups' writes separately, deciding once per interned group as it already does for names.

In Dart, ReplayDoors turns the moves into door states at a moment. MapDoor.replayActor ties each door in lib/const/map_doors.dart to its actor, checked against the level file's positions. ReplayConeAim carries the doors as a bitmask, so a cone recuts when they change. The worker loads its models with door walls.

Ground truth

Against the game's own named events, on every replay here that has those doors:

Replay Door moves decoded Named events
Lotus, 13.00 31 spins 31 MulticastPlayAnimation on a switch
Lotus, 13.09 33 spins 33
A Link, both 16 and 11 breaks 16 and 11 OnDie
Summit, 13.00 30 drops, all 1,749 to 1,758 ms 30, as counted in the design

The first rule I wrote found 27 of 31 spins. That's how I found the late-round spin and two spins back to back. The second 13.00 Lotus replay decodes with 24 spins and 13 breaks, but I didn't dump its named events, so it isn't compared. No 13.09 Summit replay exists on this machine. The corpus test passes on both the 13.00 and 13.09 corpora and checks that every move sits inside a round and lasts as long as that door takes.

In the release viewer (a driver build with scratch seek hooks that aren't in this PR, on real replays):

  • Lotus 13.09: the logged states at 52, 58.7, 61.2, 67, 165 and 215 s are rest, a quarter turn, side-on, rest, A Link broken, and rest in the next round. The pictures match.
  • Summit 13.00: B wall up at 59 s, half dropped at 60.5 s, down at 70 s.
  • Capture at 165 s, then opening the captured strategy: the page has A Link broken, one row.
  • Playback on Lotus through a spin: 1,985 frames, raster 3.6 ms at p50 and 5.5 ms at p99, build 1.3 and 2.7 ms. I didn't measure a build without this PR to compare.
  • All runs CLEAN.

A Link standing and broken

CodeRabbit pointed out that a move longer than 10 s turned a door past half a turn; it's now held at half a turn, with a test. Astra reviewed this and found no blockers. It found two real bugs, both fixed here: a spinning door cut cones side-on for its whole turn, and a recording that ended mid-move dropped the move or ended it early. It also found a test that couldn't fail, now gone, and a corpus check too loose to catch a move running into the next round, now tightened.

Tests: 8 Rust unit tests built from real timings, plus door_actor; test/replay_doors_test.dart covers states over time, the bitmask, captured rows, and the worker cutting a real Lotus model with A Link whole and broken; a capture test through the local store. 183 Rust lib tests and 138 Dart tests across the replay and door files pass.

What isn't covered

  • Summit only on 13.00.
  • The rule reads write timing, not meaning. If a patch makes doors write differently, moves would stop appearing or be wrong. The corpus test would catch "no door moved" on Lotus or Summit, not subtler drift.
  • A cloud capture wasn't run. It goes through the same page rows as a local one, and Save a map door on its rectangle #463 covered those rows in cloud.
  • Without the worker (it falls back on a failed model load), each cone cuts its own against the editor's door state, which is empty in a replay. That's the same fallback as before doors.

RetriggerView in GreptileConfidence Score: 4/5

Fix the existing Summit drop-speed issue before merging.

Findings

  1. P1 Walls drop at wrong speeds ▶

Summary

Adds replay door moves from decoding through playback and capture.

  • Replay files now include when each door moves.
  • Map doors move through their replay poses during playback.
  • Replay cones now cut against doors in their current poses.
  • Replay captures now keep the doors that changed at that moment.

No new findings were accepted. Review used source inspection; no runtime or visual checks were run.

SunkenInTime documents two accepted limits: captures save moving doors as changed, and playback without the cone worker uses the editor's empty door state.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Replay actor writes] --> B[Decoded door moves]
  B --> C[ReplayDoors at current time]
  C --> D[Draw door progress]
  C --> E[Door bitmask]
  E --> F[Cone worker]
  C --> G[Changed door rows]
  G --> H[Local or cloud capture]
Loading

Reviews (2) · Last reviewed commit: "Hold a spin at half a turn when its move..." · Reviewed by Greptile

The decoder lists when each door moved, read from when its actor is written
because the door fields have no names, plus the A Link wall's OnDie. The viewer
draws doors at the moment on screen, cones cut against them on the worker, and
a capture saves a row for each door changed, part way or not.
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 44 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: da91f612-d2d5-453e-a73e-125c32f0c5dd
Commits

Reviewing files that changed from the base of the PR and between 301b686 and 4d44a1f.

Files selected for processing: 2
  • lib/replay/replay_doors.dart
  • test/replay_doors_test.dart
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dfca1477-21a5-4c82-98c9-8cc439809087
Commits

Reviewing files that changed from the base of the PR and between 7f58eee and 301b686.

Files ignored due to path filters: 1
  • native/replay/Cargo.lock is excluded by !**/*.lock
Files selected for processing: 21
  • docs/replay-format.md
  • lib/const/map_doors.dart
  • lib/replay/replay_capture.dart
  • lib/replay/replay_cone_cuts.dart
  • lib/replay/replay_cone_worker.dart
  • lib/replay/replay_document.dart
  • lib/replay/replay_doors.dart
  • lib/replay/replay_frame.dart
  • lib/replay_view.dart
  • lib/widgets/map_door_layer.dart
  • lib/widgets/replay/replay_canvas.dart
  • native/replay/Cargo.toml
  • native/replay/src/analysis/doors.rs
  • native/replay/src/analysis/mod.rs
  • native/replay/src/collect.rs
  • native/replay/src/document.rs
  • native/replay/src/header.rs
  • native/replay/tests/corpus.rs
  • test/replay_capture_test.dart
  • test/replay_doors_test.dart
  • test/replay_frame_test.dart

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The replay decoder now records door movements in replay documents. Replay playback reconstructs door states for map doors and uses them in cone calculations, canvas rendering, and captured utility rows. The decoder version changes from 0.3.2 to 0.3.3.

Changes: Replay Door Data and Playback

Layer / File(s) Summary
Collect and serialize door movements
native/replay/src/collect.rs, native/replay/src/analysis/*, native/replay/src/document.rs, native/replay/src/analysis/mod.rs, native/replay/tests/corpus.rs, native/replay/Cargo.toml, native/replay/src/header.rs, docs/replay-format.md
The native decoder collects recognized door writes and wall-break events, derives door movements, and adds them to replay documents. The replay format documents the doors data and the updated decoder version. Unit and corpus tests validate event analysis and output.
Reconstruct replay door states
lib/const/map_doors.dart, lib/replay/replay_document.dart, lib/replay/replay_doors.dart, lib/replay/replay_frame.dart, test/replay_doors_test.dart
The replay client parses door movements, matches actors to configured map doors, and calculates door amounts and masks for each frame. Tests cover movement timing, round resets, map filtering, and missing door data.
Apply door masks to cone calculations
lib/replay/replay_cone_cuts.dart, lib/replay/replay_cone_worker.dart, lib/replay/replay_frame.dart, lib/replay_view.dart, test/replay_doors_test.dart, test/replay_frame_test.dart
Cone requests carry door masks. The worker loads map-specific door walls and uses the mask when generating cones. Tests cover mask thresholds, worker behavior, and worker startup.
Render and capture replay doors
lib/replay/replay_frame.dart, lib/replay/replay_capture.dart, lib/widgets/map_door_layer.dart, lib/widgets/replay/replay_canvas.dart, test/replay_capture_test.dart, test/replay_doors_test.dart
The replay canvas renders door amounts. Local and cloud captures append changed doors as utility rows. Tests cover captured door rows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ReplayFrameBuilder
  participant ReplayDoors
  participant ReplayCanvas
  participant ReplayConeWorker
  ReplayFrameBuilder->>ReplayDoors: at(timeMs, round)
  ReplayDoors-->>ReplayFrameBuilder: door state amounts
  ReplayFrameBuilder->>ReplayCanvas: frame with door amounts
  ReplayFrameBuilder->>ReplayConeWorker: cone request with door mask
Loading

Possibly related PRs

Merge Risk: Low · previous review 301b6

An unusually long door spin can briefly show a skewed pose. This is a bounded visual issue that can be fixed before merge or accepted for follow-up.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: displaying map doors during replay playback.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 9 functions across 6 files. (15 skipped: 15 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread lib/replay/replay_doors.dart Resolved
@SunkenInTime
SunkenInTime merged commit 912cc37 into main Nov 2, 2026
14 checks passed
Comment on lines +43 to +45
MapDoorKind.drop => move.endMs > timeMs
? (timeMs - move.startMs) / (move.endMs - move.startMs)
: 1.0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Walls drop at wrong speeds

ReplayDoors.at uses the recorded span as a full drop, but the decoder can shorten or extend that span to the recording's end. A wall starting at 40,000 ms in a recording ending at 40,875 ms appears fully down at the end, although only half its 1.75-second drop has elapsed. An extended span makes a completed drop look too slow.

Use the wall's fixed 1.75-second pace and clamp the result, as spins already do with _spinMs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant