Card-Forge / Card-Forge/forge

TokenInfo.toCard never applies the parsed mana cost, so tokens lose their cost on game-state save/load

Open
#11,772 0 comments 0 reactions 2 assignees Claimed by @Agetian View on GitHub
AI Game Mechanics
Dominant language
Java
Stars
2.7k
Forks
1.1k
Avg merge
1d 1h
Merged PRs (30d)
163

Description

**Describe the bug**

`TokenInfo` round-trips a token's mana cost through serialization and then discards it: `toCard()` never calls `Card.setManaCost(...)`. Any token restored from a serialized game state comes back with no mana cost.

This matters because a token that is a copy of a permanent copies that permanent's mana cost (CR 707.2), so the cost is a real, rules-relevant property of the token — not decoration. After a save/load it is gone, which changes anything that reads the token's mana value.

**To Reproduce**

Observed on a `forge.game.GameState` round trip, which is the code path behind Dev Mode → *Dump/Setup Game State* and Puzzle mode:

1. Get a token onto the battlefield that copies a permanent with a mana cost. I used Bloodforged Battle-Axe (`{1}`), whose combat-damage trigger creates a token copy of itself.
2. Serialize the game: `new GameState().initFromGame(game).toString()`. The token is written correctly, with its cost:
```
p0battlefield=...;t:Bloodforged Battle-Axe,P:0,T:0,Cost:{1},Color:C,Types:Artifact-Equipment,Keywords:Equip:2,Image:null
```
3. Apply that text to a game: `state.parse(lines); state.applyToGame(game);`
4. Serialize again. The same token now reads `Cost:no cost`:
```
p0battlefield=t:Bloodforged Battle-Axe,P:0,T:0,Cost:no cost,Color:C,Types:Artifact-Equipment,Keywords:Equip:2,Image:null;...
```

Everything else about the token survives — name, P/T, color, types, keywords (`Equip:2`). Only the cost is lost. (The Cat token in the same dump also loses its image/set suffix, `Image:w_1_1_cat|FDN|1|1` → `Image:w_1_1_cat`, which may be the same class of omission.)

**Expected behavior**

`Cost:{1}` after the round trip, matching what was serialized.

**Root cause**

`forge-game/src/main/java/forge/game/card/token/TokenInfo.java` (verified against current `master`).

The cost is captured, written, and parsed correctly:

- `TokenInfo(Card c)`: `this.manaCost = c.getManaCost().toString();`
- `toString()`: `sb.append("Cost:").append(manaCost).append(',');`
- `TokenInfo(String str)`: `} else if (info.startsWith("Cost:")) { manaCost = remainder; }`

But `toCard` never applies it to the card:

```java
private Card toCard(Game game, int id) {
final Card c = new Card(id, game);
c.setName(name);
c.setImageKey(ImageKeys.getTokenKey(imageName));

c.setColor(color == null ? ColorSet.fromManaCost(new ManaCost(manaCost)) : color);
c.setGamePieceType(GamePieceType.TOKEN);

for (final String t : types) {
c.addType(t);
}

c.setBasePower(basePower);
c.setBaseToughness(baseToughness);
return c;
}
```

`manaCost` appears only as a fallback for deriving the color. And since `toString()` always emits a `Color:` field, and `TokenInfo(String)` parses that into a non-null `ColorSet`, a token reconstructed from serialized text always takes the `: color` branch — so on this path `manaCost` is parsed and then **never read at all**. `Card.getManaCost()` therefore returns the default, whose `toString()` is `no cost`, which is what the next serialization writes.

**Suggested fix**

`Card.setManaCost(ManaCost)` already exists (`Card.java`), so `toCard` needs one line, something like:

```java
c.setManaCost(new ManaCost(new ManaCostParser(manaCost)));
```

guarding the "no cost"/empty case so it does not regress tokens that genuinely have no cost. Worth checking `ManaCost`'s intended parse entry point — `new ManaCost(manaCost)` is used above for the color fallback, so whichever construction is correct there should be reused.

**Version**

Reproduced on the `TokenInfo` in current `master` (the `toCard` above is verbatim from master). Found while building a headless harness that dumps and restores `GameState` between sessions; the affected code is Forge's own, not the harness's.

**Additional context**

Only reachable through the string form of `TokenInfo`, so the visible impact is limited to serialized game states — Dev Mode's Setup Game State and Puzzle mode. A puzzle whose solution depends on a copy token's mana value would behave differently once loaded from file than it did when authored.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.