Skip to content

ship/cr733 attach - #6644

Merged
matthewevans merged 6 commits into
mainfrom
ship/cr733-attach
Jul 26, 2026
Merged

ship/cr733 attach#6644
matthewevans merged 6 commits into
mainfrom
ship/cr733-attach

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 26, 2026

Copy link
Copy Markdown
Member
  • feat(engine): journal CR 701.3 attachment edits as resolved commands
  • feat(engine): journal the two battlefield-entry retags as resolved commands
  • feat(engine): journal CR 704.5d cease-to-exist as a resolved command

Summary by CodeRabbit

  • New Features
    • Added replayable “resolved command” tracking for attachments/unattachments, including exact host/timestamp transitions.
    • Battlefield entries now preserve controller overrides and the placing ability source during replay.
    • Token/object ceases and player departures are now journaled and replayed with strict invariant checks.
  • Bug Fixes
    • Improved correctness of replay exactness for host relationships, zone membership, controller/base-controller retagging, and provenance stamping.
  • Tests
    • Added integration coverage for the above resolved replay cases (including player leave).

The three attachment authorities (attach_to, attach_to_player, unattach)
wrote their graph mutation raw, so a CR 733 retained-prefix replay had no
record of who was attached to whom.

They are the same mutation parameterized by the resulting host, so they
share one ResolvedAttachmentCommand rather than three sibling variants:
Some(Object), Some(Player), and None (unattach) are leaf values of the
Option<AttachTarget> the object already stores.

CR 613.7e + CR 701.3c: attaching to a different host draws a NEW timestamp
that orders the attachment in the layer system, while a same-host re-attach
(CR 701.3b) and an unattach draw none. resulting_timestamp is therefore
Some exactly when the authority drew one, and apply_resolved_attachment
installs that value instead of re-drawing from next_timestamp.

Journaling at the three authorities covers every production attach site
for free, including the zone delivery tail's enters-attached path.
…mmands

The zone delivery tail retagged entering permanents twice with raw writes,
so a CR 733 retained-prefix replay lost both facts.

CR 110.2a controller override: apply_battlefield_entry_controller_override
wrote five fields raw. Four are persistent state (the object's
base_controller and controller, plus the zone_changes_this_turn and
battlefield_entries_this_turn snapshot controllers) and are now one
ResolvedControllerOverrideCommand. The fifth, the in-flight ZoneChanged
event, stays outside the command: events are transient carriers consumed by
the same resolution, not state a replay reconstructs. Both snapshot
POSITIONS are recorded rather than re-found, mirroring
ResolvedZoneChangeCommand::turn_zone_change_index -- CR 400.7 permits the
same object to hold several entries in one turn, so a replay-time last-match
scan could retag a different one.

CR 603.6a provenance stamp: entered_via_ability_source was the last bare
field write at the delivery-tail site, with no owning authority at all.
zones::stamp_battlefield_entry_provenance is that authority, paired with
apply_resolved_entry_provenance. Without it a replay would let
anti-recursion intervening-ifs re-trigger off their own output.

They stay separate families rather than one parameterized entry retag: CR
110.2a and CR 603.6a are different rule sections the engine resolves
independently.
zones::cease_object is the only production path that deletes an object
outright, and the CR 704.5d state-based sweep is its single caller. It wrote
state.objects.remove raw, so a retained-prefix replay left the token alive in
a zone the rules had already swept it from.

Ceasing to exist is deliberately NOT a zone change (CR 400.7) -- no event is
emitted and no whenever-exiled trigger fires -- so it cannot ride the
zone-change family and needs its own command. No characteristics are
recorded: replay removes the object the retained prefix already
reconstructed rather than rebuilding a deleted one.
@matthewevans
matthewevans enabled auto-merge July 26, 2026 02:24
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds resolved-command journaling and replay for attachment changes, controller overrides, entry provenance, object cessation, and player departure. It adds typed contracts, invariant validation, gameplay integration, replay dispatch, and CR733 integration tests.

Changes

CR733 resolved command journaling and replay

Layer / File(s) Summary
Resolved command contracts and validation
crates/engine/src/types/resolved_commands.rs
Adds five command families, replay errors, journal APIs, command variants, and serialized payload validation.
Attachment journaling and replay
crates/engine/src/game/effects/attach.rs, crates/engine/tests/integration/cr733_resolved_attachment.rs
Journals genuine attachment host transitions and replays graph changes with precondition checks.
Zone transition journaling and replay
crates/engine/src/game/zones.rs, crates/engine/src/game/zone_pipeline.rs, crates/engine/tests/integration/cr733_resolved_entry_retags.rs, crates/engine/tests/integration/cr733_resolved_object_cease.rs
Journals and replays object cessation, controller retagging, and battlefield-entry provenance stamping.
Player departure journaling and replay
crates/engine/src/game/elimination.rs, crates/engine/src/types/game_state.rs, crates/engine/tests/integration/cr733_resolved_player_leave.rs
Attributes player elimination to a dedicated leave node and replays the resolved departure state.
Resolved command dispatch coverage
crates/engine/tests/integration/cr733_resolved_commands_p2.rs, crates/engine/tests/integration/cr733_resolved_draw.rs, crates/engine/tests/integration/main.rs
Dispatches all new command variants during replay and registers the added integration modules.

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

Sequence Diagram(s)

sequenceDiagram
  participant GameResolution
  participant ResolvedRulesJournal
  participant ReplayApplier
  GameResolution->>ResolvedRulesJournal: record resolved command
  ResolvedRulesJournal->>ReplayApplier: provide serialized command
  ReplayApplier->>ReplayApplier: validate preconditions and apply exact state change
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: andriypolanski, kiannidev, lgray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the CR 733 attachment work, though it underdescribes the broader journaling and replay changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ship/cr733-attach

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/zones.rs`:
- Around line 1744-1747: The absent-object branch in the surrounding
SBA/object-cease function must not call remove_from_zone without journaling.
Since the caller reads the object immediately beforehand, assert or otherwise
require the object to exist and remove this fallback; alternatively, record the
zone-list removal through the existing ObjectCease command path before
returning.
- Around line 2035-2050: Strengthen the validation around
command.zone_change_index and command.battlefield_entry_index to verify the
indexed record’s object_id matches command.object before any controller
mutation. Keep the existing bounds checks and return MissingZoneChangeRecord or
MissingBattlefieldEntryRecord for both out-of-range and identity mismatches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca9173d1-045f-47dc-9958-7cfeea1a17e8

📥 Commits

Reviewing files that changed from the base of the PR and between 652ca8b and 4476ae1.

📒 Files selected for processing (10)
  • crates/engine/src/game/effects/attach.rs
  • crates/engine/src/game/zone_pipeline.rs
  • crates/engine/src/game/zones.rs
  • crates/engine/src/types/resolved_commands.rs
  • crates/engine/tests/integration/cr733_resolved_attachment.rs
  • crates/engine/tests/integration/cr733_resolved_commands_p2.rs
  • crates/engine/tests/integration/cr733_resolved_draw.rs
  • crates/engine/tests/integration/cr733_resolved_entry_retags.rs
  • crates/engine/tests/integration/cr733_resolved_object_cease.rs
  • crates/engine/tests/integration/main.rs

Comment on lines +1744 to +1747
let Some(object) = state.objects.get(&object_id) else {
remove_from_zone(state, object_id, zone, owner);
return;
};

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Absent-object fallback performs an unjournaled persistent write.

When the object map entry is already gone but the owner's zone list still holds the id, remove_from_zone mutates persistent state and the function returns without recording a command. A retained-prefix replay then leaves that id in the zone list — the exact divergence the ObjectCease family exists to close. Either assert the object is present (the SBA sweep is the only caller and reads it immediately before), or record the zone-list removal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/zones.rs` around lines 1744 - 1747, The absent-object
branch in the surrounding SBA/object-cease function must not call
remove_from_zone without journaling. Since the caller reads the object
immediately beforehand, assert or otherwise require the object to exist and
remove this fallback; alternatively, record the zone-list removal through the
existing ObjectCease command path before returning.

Comment on lines +2035 to +2050
if let Some(index) = command.zone_change_index {
if index >= state.zone_changes_this_turn.len() {
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingZoneChangeRecord(index),
);
}
}
if let Some(index) = command.battlefield_entry_index {
if index >= state.battlefield_entries_this_turn.len() {
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingBattlefieldEntryRecord(
index,
),
);
}
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Recorded snapshot positions are bounds-checked but not identity-checked.

If a replayed prefix produced a differently ordered zone_changes_this_turn / battlefield_entries_this_turn, an in-range index silently retags an unrelated record's controller instead of failing. The command already carries object, so compare the record's object_id before mutating and surface the existing Missing*Record error otherwise.

🛡️ Proposed precondition tightening
     if let Some(index) = command.zone_change_index {
-        if index >= state.zone_changes_this_turn.len() {
+        if state
+            .zone_changes_this_turn
+            .get(index)
+            .is_none_or(|record| record.object_id != object_id)
+        {
             return Err(
                 ResolvedControllerOverrideReplayInvariantError::MissingZoneChangeRecord(index),
             );
         }
     }
     if let Some(index) = command.battlefield_entry_index {
-        if index >= state.battlefield_entries_this_turn.len() {
+        if state
+            .battlefield_entries_this_turn
+            .get(index)
+            .is_none_or(|record| record.object_id != object_id)
+        {
             return Err(
                 ResolvedControllerOverrideReplayInvariantError::MissingBattlefieldEntryRecord(
                     index,
                 ),
             );
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(index) = command.zone_change_index {
if index >= state.zone_changes_this_turn.len() {
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingZoneChangeRecord(index),
);
}
}
if let Some(index) = command.battlefield_entry_index {
if index >= state.battlefield_entries_this_turn.len() {
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingBattlefieldEntryRecord(
index,
),
);
}
}
if let Some(index) = command.zone_change_index {
if state
.zone_changes_this_turn
.get(index)
.is_none_or(|record| record.object_id != object_id)
{
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingZoneChangeRecord(index),
);
}
}
if let Some(index) = command.battlefield_entry_index {
if state
.battlefield_entries_this_turn
.get(index)
.is_none_or(|record| record.object_id != object_id)
{
return Err(
ResolvedControllerOverrideReplayInvariantError::MissingBattlefieldEntryRecord(
index,
),
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/zones.rs` around lines 2035 - 2050, Strengthen the
validation around command.zone_change_index and command.battlefield_entry_index
to verify the indexed record’s object_id matches command.object before any
controller mutation. Keep the existing bounds checks and return
MissingZoneChangeRecord or MissingBattlefieldEntryRecord for both out-of-range
and identity mismatches.

P1 defined RulesExecutionNodeKind::PlayerLeave and the matching node ref, but
nothing in the engine ever produced one. The CR 800.4 sweep marked
is_eliminated and appended to eliminated_players with raw writes, and every
mutation it performed downstream -- owned-object exiles, control-effect
reversions, stack removals -- was attributed to whatever proposal happened to
be live when the state-based action fired.

do_eliminate now opens the leave's own execution node before the sweep and
restores the enclosing scope after it, so those downstream commands are
attributed to the departure and a replay can recognize the sweep as one
causal unit. The departure itself is one ResolvedPlayerLeaveCommand: the two
writes always move together.

The command carries no expected_old field -- "was still in the game" is the
precondition, and a stored copy could only ever hold one value. Re-applying a
departure is a typed invariant failure rather than a silent re-elimination.
CI's wasm and card-data lib builds fail with `-D dead-code` on
apply_resolved_controller_override: game::zones is `pub` only under the
test-support feature, so in a production build a pub fn nothing in the crate
calls is dead. The sibling authorities do not have this problem because
resolve_and_apply_zone_change, stamp_battlefield_entry_provenance, and
cease_object all install through their own applier.

Make the override do the same. Resolve and replay now share one mutation body
instead of two copies that can drift, and the snapshot retag is factored into
retag_battlefield_entry_snapshots so both paths install the same thing.
The fixture set life directly and relied on advance_until_stack_empty to run
the CR 704.5a state-based check, which never fired with an already-empty
stack. Resolving a real Lightning Bolt at the victim takes them to 0 through
the production pipeline and makes the check run where it actually runs.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/types/resolved_commands.rs`:
- Around line 350-357: Add verified inline CR annotations using the exact `// CR
800.4: <description>` format, after checking `docs/MagicCompRules.txt`, for the
departure command semantic boundary at
crates/engine/src/types/resolved_commands.rs:350-357, leave-node creation at
:1301-1309, recording semantics at :1602-1608, and replay validation at
:2154-2164; replace the existing `///` rule references where applicable and
ensure each annotation accurately describes the covered behavior.

In `@crates/engine/tests/integration/cr733_resolved_player_leave.rs`:
- Around line 83-86: Update the command filtering around
ResolvedRulesCommand::PlayerLeave to avoid the wildcard match: use a selective
matches! predicate for PlayerLeave commands belonging to P1, followed by
refutable let extraction, or explicitly enumerate all known enum variants.
Preserve the existing collection of matching PlayerLeave commands.
- Around line 75-133: Update the integration test around
apply_resolved_player_leave to capture a pre-elimination state snapshot and add
a player-owned object/control-effect fixture. Replay the journal entries from
that snapshot through the production dispatcher rather than directly replaying
only the departure fields, then assert the resulting CR 800.4 sweep commands
carry a RulesExecutionNodeRef::PlayerLeave cause and preserve the existing
replay/invariant checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0cf74323-0be3-4769-bc0b-dce0a77188f3

📥 Commits

Reviewing files that changed from the base of the PR and between 4476ae1 and 781eebe.

📒 Files selected for processing (8)
  • crates/engine/src/game/elimination.rs
  • crates/engine/src/game/zones.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/resolved_commands.rs
  • crates/engine/tests/integration/cr733_resolved_commands_p2.rs
  • crates/engine/tests/integration/cr733_resolved_draw.rs
  • crates/engine/tests/integration/cr733_resolved_player_leave.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/engine/tests/integration/cr733_resolved_draw.rs
  • crates/engine/tests/integration/cr733_resolved_commands_p2.rs
  • crates/engine/src/game/zones.rs

Comment on lines +350 to +357
/// One exact CR 800.4 player departure.
///
/// The departure itself is two writes — the player's `is_eliminated` flag and
/// their append to `eliminated_players` — that always move together, so they are
/// one command rather than two. Everything the CR 800.4 sweep does afterwards
/// (exiling owned objects, reverting control effects, clearing the stack)
/// already journals through its own family; this command carries the departure,
/// and the surrounding `PlayerLeave` node carries the causal grouping.

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use verified inline CR annotations for the new departure rules logic.

The new CR 800.4 references use ///; this does not meet the required exact // CR <number>: <description> annotation format.

  • crates/engine/src/types/resolved_commands.rs#L350-L357: add a verified inline CR annotation for the departure command’s semantic boundary.
  • crates/engine/src/types/resolved_commands.rs#L1301-L1309: annotate leave-node creation with the verified CR 800.4 behavior.
  • crates/engine/src/types/resolved_commands.rs#L1602-L1608: annotate recording semantics with the verified rule.
  • crates/engine/src/types/resolved_commands.rs#L2154-L2164: annotate replay validation with the verified rule.

As per coding guidelines, “Verify every implementation of MTG game rules against the relevant Comprehensive Rules section”; as per path instructions, use the exact // CR XXX.Y... form after verifying it in docs/MagicCompRules.txt.

📍 Affects 1 file
  • crates/engine/src/types/resolved_commands.rs#L350-L357 (this comment)
  • crates/engine/src/types/resolved_commands.rs#L1301-L1309
  • crates/engine/src/types/resolved_commands.rs#L1602-L1608
  • crates/engine/src/types/resolved_commands.rs#L2154-L2164
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/resolved_commands.rs` around lines 350 - 357, Add
verified inline CR annotations using the exact `// CR 800.4: <description>`
format, after checking `docs/MagicCompRules.txt`, for the departure command
semantic boundary at crates/engine/src/types/resolved_commands.rs:350-357,
leave-node creation at :1301-1309, recording semantics at :1602-1608, and replay
validation at :2154-2164; replace the existing `///` rule references where
applicable and ensure each annotation accurately describes the covered behavior.

Sources: Coding guidelines, Path instructions

Comment on lines +75 to +133
// The discriminating assertion: the departure is journaled as an exact
// resolved command. Two raw field writes record nothing here.
let leaves: Vec<_> = state
.resolved_rules_journal
.entries()
.iter()
.skip(journal_start)
.filter_map(|entry| entry.command.clone())
.filter_map(|command| match command {
ResolvedRulesCommand::PlayerLeave(command) if command.player == P1 => Some(command),
_ => None,
})
.collect();
assert_eq!(
leaves.len(),
1,
"the elimination authority must journal exactly one resolved departure"
);

// The second claim: the departure runs under a PlayerLeave node, not under an
// ambient proposal. This is what lets a replay recognize the CR 800.4 sweep as
// one causal unit.
assert!(
matches!(leaves[0].cause, RulesExecutionNodeRef::PlayerLeave(_)),
"CR 800.4: the departure opens its own execution node, got {:?}",
leaves[0].cause
);

// Replay-exactness: the recorded departure reinstalls both writes, and is not
// idempotent — a second application is a typed invariant failure rather than a
// silent re-elimination.
let mut replay = runner.state().clone();
replay
.players
.iter_mut()
.find(|player| player.id == P1)
.expect("the player record survives elimination")
.is_eliminated = false;
replay.eliminated_players.retain(|player| *player != P1);

engine::game::elimination::apply_resolved_player_leave(&mut replay, &leaves[0])
.expect("the recorded departure must replay");
assert!(
replay
.players
.iter()
.find(|player| player.id == P1)
.expect("the player record survives elimination")
.is_eliminated,
"replay installs the recorded departure"
);
assert!(
replay.eliminated_players.contains(&P1),
"replay installs the eliminated-list append"
);
assert!(
engine::game::elimination::apply_resolved_player_leave(&mut replay, &leaves[0]).is_err(),
"a departure is not idempotent: re-applying it is a typed invariant failure"
);

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Exercise retained-prefix replay from before the departure.

This only replays the two direct fields after cloning the post-sweep state; all CR 800.4 effects are already present. Add a player-owned object/control-effect fixture, replay journal entries from a pre-elimination snapshot through the production dispatcher, and assert the sweep commands carry the PlayerLeave cause. Otherwise an incorrectly scoped or unreplayable departure sweep still passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/cr733_resolved_player_leave.rs` around lines
75 - 133, Update the integration test around apply_resolved_player_leave to
capture a pre-elimination state snapshot and add a player-owned
object/control-effect fixture. Replay the journal entries from that snapshot
through the production dispatcher rather than directly replaying only the
departure fields, then assert the resulting CR 800.4 sweep commands carry a
RulesExecutionNodeRef::PlayerLeave cause and preserve the existing
replay/invariant checks.

Source: Path instructions

Comment on lines +83 to +86
.filter_map(|command| match command {
ResolvedRulesCommand::PlayerLeave(command) if command.player == P1 => Some(command),
_ => None,
})

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Avoid the wildcard match on ResolvedRulesCommand.

ResolvedRulesCommand is a known enum; the wildcard prevents compiler assistance when variants change. Use a selective matches! filter plus a refutable let extraction, or enumerate variants explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/cr733_resolved_player_leave.rs` around lines
83 - 86, Update the command filtering around ResolvedRulesCommand::PlayerLeave
to avoid the wildcard match: use a selective matches! predicate for PlayerLeave
commands belonging to P1, followed by refutable let extraction, or explicitly
enumerate all known enum variants. Preserve the existing collection of matching
PlayerLeave commands.

Sources: Coding guidelines, Path instructions

@matthewevans
matthewevans added this pull request to the merge queue Jul 26, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

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