fix(engine): emit the legacy tuple for PerSource damage thresholds - #7479
Conversation
#7476 gave `damage_amount` a scope axis and added a compatibility deserializer, so data written before the axis still loads. The migration was one-way: nothing preserved the write side. `PerSource` serialized as `{"comparator":"GE","threshold":6}` where every build predating the axis emits and expects `["GE", 6]`. Eliding the `PerSource` default keeps the object minimal but does not preserve its shape, so two comments claiming pre-axis cards "serialize byte-identically" were false - the assertion 15 lines below one of them pinned the object form. This matters because the shape reaches persistence with no version gate to reject it early. `PersistedSession.state` is a `PersistedGameState` (server-core/src/persist.rs:23) and `GameObject.trigger_definitions` is serialized, so a build carrying the axis writes rows that an older build fails to deserialize - taking the whole session restore with it. The three consumers are the ones already named on `deserialize_damage_amount_compat`: browser IndexedDB saved games, the phase-server session store, and the shared card fixture. `serialize_damage_amount_compat` is the write-side counterpart. PerSource emits the legacy tuple; only WholeEvent emits the object, and no pre-axis reader can represent that pole anyway. Information-lossless, and it makes the byte-identical claim true rather than deleting it. Modeled on the `serialize_multi_target_min` / `deserialize_multi_target_min` pair in the same file, which collapses a Fixed variant to a bare int the same way. Also pins `#[serde(default)]` on `DamageAmountThreshold::scope` with an object row carrying no `scope` key. That shape is reachable, not hypothetical: any build between the axis landing and this serializer wrote exactly it. Reported by CodeRabbit on #7476, which merged before the finding could be addressed in that PR.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan includes up to 2 reviews per rolling hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe ChangesDamage amount compatibility
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This localized serialization fix is merge-ready after normal checks and review; no actionable merge-blocking risk remains. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Generated for head Parse changes introduced by this PR✓ No card-parse changes detected. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Follow-up to #7476, addressing CodeRabbit's Major finding on that PR. #7476 merged
through the queue before the finding could be addressed there.
The gap
#7476 gave
damage_amounta scope axis and addeddeserialize_damage_amount_compatso pre-axis data still loads. The migration was one-way — nothing preserved the
write side.
PerSourceserialized as{"comparator":"GE","threshold":6}, where every buildpredating the axis emits and expects
["GE", 6]. Eliding thePerSourcedefaultkeeps the object minimal, but it does not preserve the shape, so two comments
claiming pre-axis cards "serialize byte-identically" were false — and the assertion
15 lines below one of them pinned the object form.
Why it matters
The shape reaches persistence with no version gate to reject it early:
PersistedSession.stateis aPersistedGameState(crates/server-core/src/persist.rs:23)GameObject.trigger_definitionsis a serialized fieldSo a build carrying the axis writes rows an older build fails to deserialize, taking
the whole session restore with it. The three consumers are the ones already named on
deserialize_damage_amount_compat: browser IndexedDB saved games, the phase-serversession store, and the shared card fixture.
The fix
serialize_damage_amount_compat, the write-side counterpart.PerSourceemits thelegacy tuple; only
WholeEventemits the object — and no pre-axis reader canrepresent that pole anyway. Information-lossless, and it makes the byte-identical
claim true rather than deleting it.
Modeled on the
serialize_multi_target_min/deserialize_multi_target_minpair inthe same file, which collapses a
Fixedvariant to a bare int the same way.Also pins
#[serde(default)]onDamageAmountThreshold::scopewith an object rowcarrying no
scopekey — CodeRabbit's nitpick. That shape is reachable rather thanhypothetical: any build between the axis landing and this serializer wrote exactly it.
Verification
Tested on a worktree carrying the identical edit:
cargo fmt --all -- --checkcleanserialize_withfails the assertion with exactly thebreak in question —
left: Object {"comparator": "GE", "threshold": 6}vsright: Array ["GE", 6]Summary by CodeRabbit
Bug Fixes
Tests