Skip to content

M8.1 — What to Persist

What you'll learn

  • The distinction between state you must save and state you can reconstruct on load, and how to decide which is which.
  • A concrete persistence list for the Emberdelve slice: level/XP, stats base, inventory, equipment, currency, position, and the RNG seed.
  • Why saving the minimum and reconstructing the rest is both correct and a smaller bug surface.
  • How to read save design as a state-coverage problem (a QA bridge): the save is a snapshot; the test is whether the round-trip reproduces the original state.

How it applies

  • A save is a contract with the player. Quit, relaunch, and the run is exactly as you left it. An ARPG run represents real time invested — losing it, or restoring it subtly wrong, is among the worst failures a game can have. Deciding precisely what to capture is the first step to honoring that contract.
  • Saving too much is as bad as saving too little. Persisting derived or transient state (computed stats, live node references, the position of every loot drop) bloats the file, couples the save to the scene structure, and creates inconsistencies when the saved derived value disagrees with what would be recomputed. The discipline is to save sources and recompute derivations.
  • Negative space matters. What you deliberately don't save — enemy positions, in-flight knockback, the current attack animation frame — is reconstructed fresh, which is usually correct (you don't want to resume mid-swing) and always simpler.
  • State coverage, named. The save/load round-trip is a state-coverage test: enumerate the state that defines a run, save it, reload, and assert each piece returned. A senior tester already thinks this way; this chapter names the persisted set so the round-trip is checkable.

Try it first

Before reading on: the player quits mid-run and relaunches. List everything that must be the same when they return. Then, for each item, ask: is this a source of truth, or is it derived from other state? Could you rebuild it on load instead of storing it? Spend a few minutes writing the list and splitting it into "save" and "reconstruct" — the split is the whole lesson, and the obvious first instinct (save everything) is the one to interrogate.

Concepts

Save sources, reconstruct derivations

State in a running game falls into three buckets:

  1. Sources — the irreducible facts of the run that nothing else can regenerate: the player's level and XP, the base StatBlock, the items in the bag and the equipment slots, currency, the player's position, and the run's RNG seed. These must be saved.
  2. Derivations — values computed from sources: the player's derived stats (base + equipped item modifiers, M5.4), current max health. These must not be saved — recompute them on load from the sources. Saving a derivation risks it disagreeing with what recompute would produce (after a balance patch, say), and a save that contradicts the code is a corruption you authored.
  3. Transients — live, moment-to-moment state that should reset: enemy positions and health, in-flight knockback, the current animation frame, loot lying on the ground. These are reconstructed fresh — the world repopulates from spawners and the player resumes standing still, which is what the player expects after a reload.

The rule compresses to: save the smallest set of sources from which the run can be rebuilt; reconstruct everything else.

Current HP: source or derivation?

A useful edge case. Max health is a derivation (base + modifiers, recomputed on load). But current HP — how hurt the player is right now — is a source: you can't recompute "the player was at 40% health." So current HP is saved, while max health is recomputed and current HP is then clamped to the new max. This is the kind of per-field judgment the save list forces you to make explicit.

The Emberdelve persistence list

For this slice, the sources to save:

State Why a source Notes
level, xp progress; not derivable M5.3
base StatBlock the character sheet M5.1; the base, not derived
current HP "how hurt am I" clamp to recomputed max on load
inventory items the bag's contents M7.1; each item's data (M6.1)
equipment per slot what's worn M7.2; the items, by slot
currency (Ember) accumulated wealth a GameState field
player position where to resume optional; some games reset to town
RNG seed + state reproducible future rolls M5.2/M6.3; see below

What is not saved: enemy/loot/world state (respawned), derived stats (recomputed), UI state (rebuilt), the modifier list (rebuilt from base + equipped items on load).

Saving the RNG

The run's GameState.rng (M1.4) drives drops and damage. Whether to persist it depends on the design: saving its seed and internal state makes future rolls deterministic across a save/load, which prevents "save-scumming" a bad drop by reloading (the reload reproduces the same roll). If you instead want each session fresh, save only enough to re-seed. Either is valid; the point is that the RNG is state, and deciding its persistence is a deliberate anti-exploit/design choice, not an oversight.

Serializing items

Each item (M6.1) is a base type plus rolled affixes. To save it, you record enough to rebuild it: which base type (by an id/path), its rarity, and its rolled affix values. On load you reconstruct the ItemData from the base type and reapply the saved affix values. You do not serialize the live Resource object graph blindly — you save the recipe (base id + rolls), which is compact and survives the base type being re-authored. M8.2 implements this; M8.1's job is to recognize that an item's source state is "base id + rarity + rolled affix values," not the whole object.

Walkthrough

This chapter is design, not code — the implementation is M8.2. The deliverable is an explicit, written persistence list for your project.

  1. Write down every piece of run state, then tag each save, reconstruct, or derive. Use the table above as the starting point and add anything your project has (e.g., quest flags, unlocked areas).
  2. For each save item, note its source form — the minimal data to rebuild it. For items, that is "base type id + rarity + rolled affix values," not the live object.
  3. For each derive item (max health, derived stats), note that load will recompute it (M5.4), never read it from the file.
  4. Decide the RNG policy: persist its state (deterministic future, anti-save-scum) or re-seed each session. Write down the choice and the reason.
  5. Decide the position policy: resume at saved position, or reset to a safe spawn. Write it down.
  6. Keep this list — M8.2 serializes exactly the save set, and M8.3's negative tests check the round-trip against it.

Optional sanity check

Take your list and, for each save item, ask: "if I delete this from the file, what breaks on load?" If nothing breaks (the value is recomputed), it shouldn't have been on the save list — move it to derive. For each derive item, ask: "if I do save this and the code later computes it differently, what happens?" The answer (the file contradicts the code) is why derivations aren't saved. This two-way interrogation is the state-coverage discipline that keeps the save minimal and correct.

Self-check quiz

Q1 — Why save the base StatBlock and recompute derived stats on load, rather than saving the derived stats directly?

A. Derived stats are larger to store. B. Derived stats are computed from the base plus equipped item modifiers; saving them risks the file disagreeing with what recompute would produce (e.g., after a balance change), so you save the source and rebuild the derivation. C. The engine forbids saving derived values. D. Base stats never change, so they're easier.

Reveal answer

B. A derivation should always be reproducible from its sources; persisting it creates a second, independent copy that can contradict the recompute (especially across patches), which is a self-authored corruption. Save the base + equipped items (sources), recompute stats on load. A is negligible. C is false. D is irrelevant to the correctness reason.

Q2 — Current HP is saved, but max health is not. Why the asymmetry?

A. Max health doesn't matter. B. Max health is a derivation (base + modifiers, recomputable on load); current HP is a source ('how hurt am I') that can't be recomputed, so it's saved and then clamped to the recomputed max. C. Current HP never changes. D. The engine saves HP automatically.

Reveal answer

B. Max health can be rebuilt from base + equipped modifiers, so it's a derivation; current HP is an irreducible fact of the run state with no formula behind it, so it must be saved (and clamped to the new max on load in case gear changed the max). A, C, D are false.

Q3 — What is the right source form to save for a rolled rare item?

A. A screenshot of its tooltip. B. Its base type id, rarity, and rolled affix values — the recipe to rebuild the ItemData — rather than blindly serializing the whole live Resource object. C. Only its display name. D. Its derived contribution to the player's stats.

Reveal answer

B. An item's irreducible state is the recipe: which base type, what rarity, which affix values rolled. That rebuilds the exact item compactly and survives the base type being re-authored. A is absurd. C loses the affixes (the item's identity). D is a derivation, not the item's source state.

Integration question

Q4 — open

Apply the save/reconstruct/derive split to the entire Emberdelve slice: name what is saved, what is reconstructed fresh, and what is derived on load, and justify each category. Then explain why this minimal-source discipline produces both a smaller save file and a smaller bug surface than 'save everything,' connecting it to the state-coverage testing a QA professional would run.

Reveal expected answer

Saved (sources): level and XP (M5.3, irreducible progress); the base StatBlock (M5.1, the character sheet, not derived); current HP (a fact, not recomputable); inventory contents and equipment-by-slot as recipes — base type id + rarity + rolled affix values (M6.1/M7); currency; the player's position (if resuming there); and the RNG state (M5.2/M6.3, by policy). Reconstructed fresh (transients): enemies and their positions/health (respawned by spawners, M4.4), loot on the ground, in-flight knockback and animation frames, UI state — the player resumes standing still in a repopulated world, which is what they expect. Derived on load: the player's max health and the full derived StatBlock, rebuilt by M5.4's recompute from the saved base plus the modifiers of the loaded equipped items; the modifier list itself, rebuilt from equipment. The justification per category: sources can't be regenerated, so they must be stored; transients should reset (resuming mid-swing or with the old enemy layout would be wrong and fragile); derivations must be recomputed so they never contradict the code. This minimal-source discipline yields a smaller file (recipes and a few numbers, not the whole object graph and world) and a smaller bug surface for two reasons: fewer fields means fewer things to serialize incorrectly, and — critically — by never persisting derivations, the save can't drift from the code (a saved derived stat would become wrong the moment a formula or item changes, a corruption you authored). For state-coverage testing, the saved set is the test matrix: a QA professional enumerates each saved source, saves, reloads, and asserts each returned (level, XP, HP clamped correctly, each bag and equipped item rebuilt identically, currency, position, RNG), plus asserts that derivations match a fresh recompute and transients reset — turning "does the save work" into a finite, checkable list of round-trip assertions.