Skip to content

Sourceless rule triggers reach the stack unnamed, so the client renders a literal "Unknown" card #7520

Description

@cuinhellcat

A stack entry for an inherent rule ability reaches the client with an empty source name, and the client fills the hole with a literal "Unknown" of its own — then asks the card-image layer for a card by that name.

Reproduction

Reported from a real game: increasing speed off combat damage briefly shows a blank card on the stack.

Measured in the engine (inherent_rule_trigger_display_name.rs, red against d013272c1):

trigger StackEntryKind::TriggeredAbility::source_name
monarch end-step draw (CR 725.2) ""
speed increase (CR 702.179d) ""

Why the name is empty

CR 113.8 says an ability's source is the object it exists on. Four rules make an explicit exception and mint abilities with no source at all, each in the same words:

  • CR 725.2 — the monarch: "These triggered abilities have no source."
  • CR 726.2 — the initiative: "These triggered abilities have no source."
  • CR 728.1 — rad counters: "This ability has no source."
  • CR 702.179d — speed: "This ability has no source."

The engine models that faithfully, pushing them with the ObjectId(0) no-source sentinel. source_name is then filled by looking source_id up, which finds nothing, and the field's own doc comment records the outcome as intended: "Empty when the source has no name (synthetic game-rule triggers like monarch draw use ObjectId(0))."

Why that is a defect and not a wire detail

client/src/components/stack/StackEntry.tsx:

const sourceName = details?.source_name || triggerSourceName || sourceObj?.name || "Unknown";
const { src, isLoading } = useCardImage(sourceObj ? imageLookup.name : sourceName, { ... });

"Unknown" is game-facing text no rule produces, invented by the display layer because the wire carried nothing — the frontend deriving content, which CLAUDE.md forbids outright. It then travels into useCardImage, which searches for a card named "Unknown", finds none, and CardArtFallback paints a card-shaped box labelled "Unknown". That is the blank card the player sees.

Scope

The class is exactly the four sourceless rule triggers above, not one card. Monarch steal and initiative steal (CR 725.2 / CR 726.2) are unaffected — they carry the damaging creature as a real source.

Also found

Two engine descriptions cite CR 725.2 for an initiative ability; the initiative is CR 726.2 (verified against docs/MagicCompRules.txt:6250). CR 725.2 is the monarch.

Fix

PR attached. The engine names these abilities; the client's invented literal is deleted rather than replaced.

Open gap, deliberately not closed here

The stronger invariant "every stack entry names its source" is false today, and the PR states the measurement rather than implying otherwise: asserting it fails 53 existing tests (delayed triggers, CR 603.7d, carry neither a captured source nor a description) and a middle form fails 10 more (fixtures naming a source id they never insert). Both groups are still nameable by a viewer — the first through source_id, the second in any real game — so neither is this defect. Closing that is separate work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions