Skip to content

Fix mobile NPE when cloning Adventure creatures with card images disabled#10349

Open
MostCromulent wants to merge 1 commit intoCard-Forge:masterfrom
MostCromulent:fix/adventure-clone-npe
Open

Fix mobile NPE when cloning Adventure creatures with card images disabled#10349
MostCromulent wants to merge 1 commit intoCard-Forge:masterfrom
MostCromulent:fix/adventure-clone-npe

Conversation

@MostCromulent
Copy link
Copy Markdown
Contributor

@MostCromulent MostCromulent commented Apr 10, 2026

Fix mobile NPE when cloning Adventure creatures with card images disabled

Fixes #10036

Problem

When Superior Spider-Man copies an Adventure creature with card images disabled on mobile, the renderer crashes with a NullPointerException on CardStateView.getName().

Root cause: CardView.updateState() sets Secondary=true (correctly checking clone states) but sets AlternateState=null (because getAlternateState() only checks original states, not clone states). The mobile renderer assumes these are consistent.

Fix

Two changes, both required:

  1. CardView.updateState() — when alternateState is null but the card has a Secondary clone state, use it. This closes the inconsistency between Secondary and AlternateState in the view. Only triggers for clones of Adventure creatures; non-cloned Adventure cards already have both set correctly.

  2. CardImageRenderer.setTextBox() — when the backup card's alternate state is null, fall back to the in-game card. The backup is the clone's paper card (Spider-Man), which isn't an Adventure. The in-game card has the correct Adventure state from fix 1.

Neither fix alone is sufficient: fix 1 sets card.getState(true) but the renderer reads from the backup; fix 2 falls back to the card but needs fix 1 for the card's state to be set.

Note: workaround vs root cause

This is a targeted workaround. The underlying issue is that Card.getAlternateState() / hasAlternateState() / getAlternateStateName() don't check clone states, while getState() / hasState() do. A root cause fix would make those three methods clone-aware:

// hasAlternateState() — currently counts original states map size
public final boolean hasAlternateState() {
    CardCloneStates clStates = getLastClonedState();
    if (clStates != null) {
        return clStates.size() > 1;
    }
    int threshold = states.containsKey(CardStateName.FaceDown) ? 2 : 1;
    return states.size() > threshold;
}

// getAlternateState() — currently reads from states.get(), not clone-aware getState()
public CardState getAlternateState() {
    if (hasAlternateState() || isFaceDown()) {
        return getState(getAlternateStateName());
    }
    return null;
}

// getAlternateStateName() — the hard one. Currently uses getRules().getSplitType()
// which returns the HOST card's rules, not the cloned card's. Would need to infer
// the alternate state from clone state keys instead:
CardCloneStates clStates = getLastClonedState();
if (clStates != null) {
    if (clStates.containsKey(CardStateName.Secondary)) return CardStateName.Secondary;
    if (clStates.containsKey(CardStateName.Flipped)) return CardStateName.Flipped;
    if (clStates.containsKey(CardStateName.Backside)) return CardStateName.Backside;
    // split cards need existing LeftSplit/RightSplit toggle logic...
}

These methods are called from 89 sites across 25 files (game logic, AI, UI on both platforms, network), so changing what they return for cloned cards could have hard-to-predict side effects across clone scenarios (flip, transform, split, etc.). The workaround scopes the fix to exactly where the inconsistency causes a problem.

Testing

  1. Start a mobile game with card images disabled
  2. Get Superior Spider-Man to copy an Adventure creature (e.g. Bonecrusher Giant from graveyard)
  3. Before: NPE crash when the clone renders
  4. After: Clone renders correctly with Adventure layout
Screenshot 2026-04-10 222507

🤖 Generated with Claude Code

When a clone (e.g. Superior Spider-Man) copies an Adventure creature,
the CardView's AlternateState was null because getAlternateState() only
checks original card states, not clone states. The mobile renderer then
crashes calling getName() on the null state when rendering with card
images disabled.

Two fixes: CardView.updateState() falls back to the Secondary clone
state, and CardImageRenderer falls back from the backup to the in-game
card when the backup lacks the adventure state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tool4ever
Copy link
Copy Markdown
Contributor

hmn not sure why this isn't a problem on Desktop though - might be better if any fallback logic lived in the same places?

@Hanmac
imo that whole updateState method and the backup business seems way too messy :/

@MostCromulent
Copy link
Copy Markdown
Contributor Author

hmn not sure why this isn't a problem on Desktop though - might be better if any fallback logic lived in the same places?

AI analysis:

Desktop doesn't hit this because its renderer (FCardImageRenderer) calls card.getState(true) directly on the in-game CardView — it never goes through a backup card. The mobile renderer uses card.getBackup().getState(true) instead, reading from the clone host's paper card (Spider-Man), which isn't an Adventure, so getState(true) returns null → NPE.

The getBackup() pattern was added to mobile in d8842f8 (Sep 2021) to fix how Adventure cards rendered in the fallback card renderer (when card images are disabled). The clone scenario wasn't considered at that point. If the fix were mobile-only it would live entirely in CardImageRenderer.java, but the CardView.updateState() change closes the inconsistency between hasSecondaryState() returning true and getState(true) returning null for clones, which is a view-layer bug regardless of which renderer reads it.

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.

Clone of Adventure crashes GUI

2 participants