TokenInfo.toCard never applies the parsed mana cost, so tokens lose their cost on game-state save/load
- 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
Assessment
This issue has not been assessed yet.