Files
aetherbound-guild/docs/product/SAVE_AND_FAILURE_CONTRACT.md
T

26 KiB

Aetherbound Guild: Save And Failure Contract

Authority: persistence boundaries, transaction idempotency, interruption, backup, migration, corruption handling, duplicate protection, clock behavior, defeat consequences, and deterministic recovery.

Revision: design-batch-01 / 2026-08-10

GAME_PRODUCT_CONTRACT.md owns the player promise. SYSTEMS_AND_BATTLE.md owns encounter rules. ECONOMY_AND_BALANCE.md owns the value and multiplier formulas used here. This document has priority whenever a result is ambiguous because preserving an authoritative committed state is more important than a presentation sequence.

1. Persistence Invariants

  1. A confirmed action is applied exactly once or not at all. There is no state in which its cost is applied without its declared result.
  2. Closing, suspending, crashing, losing power, changing devices, or retrying a request cannot duplicate a reward, consume a second resource, reroll a route, or change a deterministic battle outcome.
  3. The latest valid local commit remains playable offline. Cloud and platform services are optional copies, never gameplay authority.
  4. Offline gameplay progress is exactly zero. There are no real-time rewards, construction clocks, energy recovery, expedition simulation, or daily claims.
  5. A first-campaign defeat can lose route opportunity and Unsecured Cache value, and can add Strain, but cannot delete a named adventurer, a banked item, a profession unlock, Mastery, Anchor restoration, or the save slot.
  6. Migration is copy-first and reversible. A new build never rewrites the only known-valid save.
  7. Recovery is deterministic and inspectable. When data cannot be trusted, the game restores the newest valid backup and identifies the discarded boundary.
  8. Save, delete, overwrite, import, cloud-conflict, and risky recovery actions are fully operable by touch, mouse, keyboard, and controller and are never color-only or hold-only.

2. Save Topology And Envelope

The product supports three independent campaign slots. Each slot has one active commit, three rotating safe backups, one latest Anchor checkpoint, one optional pre-migration copy, and a bounded operation journal.

profile_save
  profile_schema_version
  settings_revision, language, accessibility, input mappings
  slot_index[]
  platform_entitlement_cache (non-authoritative)

campaign_slot
  slot_id, campaign_id, lineage_id
  save_schema_version, ruleset_version, content_revision
  commit_id, parent_commit_id, commit_sequence
  created_at_wall_clock, last_seen_wall_clock (display only)
  deterministic_generation_root_seed
  difficulty_contract, selected_reweave_laws
  campaign_state, guild_state, roster_state, inventory_state
  expedition_state or null
  encounter_state or null
  operation_journal, achievement_outbox
  payload_sha256, envelope_crc32

campaign_id, lineage_id, operation IDs, stable content IDs, and seeds are 128-bit values rendered as lowercase hexadecimal. commit_sequence is a local monotonic unsigned integer. Wall-clock fields are never used to resolve game rules or conflict winners.

The envelope has two integrity checks: CRC32 catches incomplete media writes; SHA-256 covers the canonical payload and catches any other unexpected change. Neither is treated as anti-cheat or security. The game must not refuse an offline save because it lacks a server signature.

3. Versioning And Compatibility

save_schema_version, ruleset_version, and content_revision serve different purposes:

Field Changes when Compatibility rule
save_schema_version Field layout or serialization changes Requires an ordered migration step
ruleset_version Formula, targeting, economy, or failure behavior changes Existing encounters resume under their stored ruleset until a safe boundary
content_revision Stable content rows or localized resources change Stable IDs must resolve or enter legacy recovery

A battle or encounter transaction always finishes under the stored ruleset_version. A newer ruleset becomes active only at the Guild or after an Anchor commit, before a new route node is generated. The migration UI states that the next expedition uses revised rules; it never changes a battle already in progress.

Supported compatibility is current schema plus the previous three shipped schemas. Older saves may still be migrated by a tested chain, but lack of such a chain is a blocking error with an export option, not permission to reset.

4. Transaction And Operation Model

4.1 Operation Identity

Every state-changing player or system action has a stable operation_id scoped to the campaign:

operation_id = hash(campaign_id, commit_sequence_at_intent,
                    operation_kind, source_stable_id, local_nonce)

The operation journal stores:

operation_id, operation_kind, intent_hash
status: pending | applied | rejected
pre_commit_id, result_commit_id
cost_delta, result_delta, rejection_reason
created_tick or route_sequence

The same operation_id with the same intent_hash returns the stored result. The same ID with a different intent is rejected as operation_id_conflict and applies nothing. Journal entries remain until two later Anchor checkpoints and then compact into an immutable applied-ID set. The applied-ID set is retained for the entire campaign slot.

4.2 Atomic Commit Procedure

Every transaction follows this order:

  1. Validate the current commit, legal state, cost, target, and intent hash.
  2. If the operation is already applied, return its recorded result.
  3. Build the complete next payload in memory; do not mutate the active payload.
  4. Add the pending operation and all cost/result deltas to that payload.
  5. Validate schema, content references, resource non-negativity, item ownership, roster uniqueness, route ancestry, and encounter hash.
  6. Serialize to a new file, write checksum, flush file and containing directory, then atomically replace the active pointer.
  7. Mark the operation applied in the same new commit or an immediate child commit. On recovery, a pending operation with an applied delta is finalized; one without a complete delta is discarded.
  8. Rotate backups only after the new active commit reads back and validates.

An animation, sound, platform achievement, or cloud upload occurs after step 8. It cannot determine whether the operation succeeded.

4.3 Transaction Boundaries

Action Commit point Duplicate response
Equip/move/change loadout Confirmed legal state at Guild/Prepare Return already-equipped result
Buy/recruit/craft/upgrade/salvage Cost and result commit together Return receipt; do not spend again
Reveal/choose route node Node seed and choice commit before transition Reopen identical preview/node
Start encounter Initial encounter snapshot and combat seed commit Resume same countdown
Fire Directive Accepted target and simulation tick commit One charge and one event only
End encounter Objective outcome and report commit Reopen same report
Claim reward Chosen stable ID and Cache delta commit Return same claimed card
Secure Cache/Anchor Cache removal and banked ledger commit together Return same bank receipt
Change difficulty/Reweave law Guild-only confirmed commit Reopen selected contract

5. Encounter Lifecycle And Save Points

The encounter state machine is:

preview -> prepared -> committed -> countdown -> resolving
        -> outcome_locked -> report -> reward_pending -> reward_applied
        -> route_committed
  • preview: leaving changes nothing. The preview seed and visible choices are already stored, so reopening cannot reroll them.
  • prepared: loadout edits are saved, but the encounter may still be cancelled.
  • committed: battle seed, company snapshot, difficulty, and failure exposure are fixed. No equipment/profession edits are accepted.
  • resolving: deterministic snapshots and a command journal support resume.
  • outcome_locked: victory, defeat, or retreat predicate is immutable.
  • report: presentation may be skipped without changing rewards.
  • reward_pending: cards are fixed; no reward is owned yet.
  • reward_applied: the chosen reward exists once in the Unsecured Cache or bank.
  • route_committed: the next route state owns the result; encounter detail may be compacted after the next Anchor backup.

Safe save points occur after every confirmed Guild action, route-node reveal, node commit, accepted Directive, outcome lock, reward claim, Cache operation, and Anchor restoration. The game also writes a battle resume snapshot every ten simulation ticks (one simulation second). These snapshots are bounded to the current encounter and are not exposed as manual save-scumming slots.

Save and Quit completes the current batch and writes the same active commit; it does not create a branch, reroll point, or separate manual-save lineage.

6. Battle Interruption And Deterministic Resume

An encounter resume snapshot stores:

encounter_instance_id, ruleset_version, content_revision
root combat seed and per-stream draw indices
simulation_tick, fixed-point unit state, slots, statuses, threat
event queue, cooldowns, readiness, Aether, Command state
objective/phase state, Fracture Clock
accepted command journal through simulation_tick
snapshot_hash, previous_snapshot_hash

On normal suspend, the game completes the current 0.1-second batch, writes a snapshot, then acknowledges suspend. If the process is killed first, recovery loads the newest valid snapshot and replays accepted commands from the previous valid snapshot. Replayed commands carry their original ticks and operation IDs.

Recovery never substitutes current wall-clock time. A battle suspended for a week resumes at the same simulation tick and next event. Display interpolation, camera, particles, and non-authoritative audio restart from the recovered state.

If the latest snapshot is corrupt:

  1. validate snapshots newest to oldest;
  2. load the newest valid snapshot with a matching encounter and ruleset;
  3. replay journaled commands and deterministic streams to the last committed tick;
  4. compare the reconstructed state hash with the stored checkpoint hash;
  5. if it matches, resume and record snapshot_recovered;
  6. if no snapshot matches, roll back to committed and restart the identical encounter seed with the same loadout, automatically replay every valid journaled command at its original tick, and resume after the last such tick with no cost/reward changes.

Restarting from committed is a recovery action, not a player-selectable retry. It is unavailable after an outcome has been locked.

7. Duplicate Action And Reward Protection

7.1 Input Debounce Is Not Authority

UI debounce may prevent repeated taps, but correctness relies on operation IDs. Touch double-taps, controller repeat, network retry, OS lifecycle replay, and cloud reconciliation all return the first committed result.

For a Directive:

accept(command_id, target, tick):
  if command_id in applied_ids: return stored_result
  if charge < 1 or target illegal: reject without cost
  otherwise spend 1 charge and enqueue exactly one command at tick+1

For a reward claim:

claim(reward_operation_id, card_id, disposition):
  verify card_id belongs to fixed draft
  verify operation has no applied result
  remove draft, add exactly one Cache/bank entry, append applied ID
  commit all fields atomically

No inventory repair routine may create a second item to compensate for a display problem. It first resolves ownership from the operation journal.

7.2 Achievement And Platform Outbox

Achievements are derived from committed gameplay facts. When offline, up to 100 idempotent platform events are queued. If the bound is reached, older events are re-derived from campaign facts later; gameplay never blocks and no reward is lost. Platform success or failure cannot modify Crowns, items, XP, Mastery, route state, or completion state.

8. Backup, Corruption, And Recovery

8.1 Backup Rotation

Each campaign slot keeps:

  • active: newest fully validated commit;
  • backup_1..3: three prior safe commits at distinct transaction boundaries;
  • anchor_checkpoint: latest valid Anchor commit, retained until a newer Anchor;
  • pre_migration: original file before a schema/ruleset migration;
  • quarantine: corrupt bytes and a diagnostic manifest, never auto-deleted.

Backups are written only inside the slot's explicit storage directory. A failed rotation leaves the active file and previous backups untouched. Storage pressure first removes old diagnostic logs, never the active or latest Anchor backup.

8.2 Validation Order

On load, validate in this order:

  1. envelope parse and size bounds;
  2. CRC32 and SHA-256;
  3. schema and required fields;
  4. campaign/lineage/parent commit relationships;
  5. resource bounds and nonnegative balances;
  6. stable content IDs and item ownership uniqueness;
  7. roster and profession topology;
  8. expedition route ancestry and committed node;
  9. encounter snapshot and operation-journal consistency.

A presentation preference error falls back to defaults without touching the campaign. A gameplay-state error invokes backup recovery. The game never labels a save corrupt merely because optional cloud or platform services are offline.

8.3 Recovery Result

When recovery selects a backup, the player sees:

  • which slot was affected;
  • the recovered boundary (Guild, Anchor, node, battle snapshot, or report);
  • the number and kinds of operations after that boundary that could not be verified;
  • options to continue from the recovered state, inspect technical details, or export a diagnostic copy;
  • an explicit statement that the quarantined file was preserved.

The default focus is Continue recovered save, not delete or overwrite. A recovery message may not claim that all progress was preserved unless the active and reconstructed commit hashes match.

9. Migration Contract

Migration runs only at startup or the Guild, never during an expedition or encounter. The procedure is:

  1. Validate and preserve the original as pre_migration.
  2. Build a migration plan of adjacent version steps; skipping a step is illegal.
  3. Resolve stable content-ID mappings and list any legacy rows.
  4. Apply each pure migration function to a copy.
  5. Validate every invariant and recompute derived values from source fields.
  6. Write a new lineage child commit with migration IDs and old/new hashes.
  7. Read back, then switch the active pointer. Preserve the old copy until two valid Anchor commits under the new version.

Migrations never generate random values. If a removed item ID has no direct replacement, it becomes a legacy_sealed_item retaining original stable ID, band, tier, and salvage value. It cannot be equipped; at the Guild, the player chooses one of two deterministic same-band replacements or keeps it sealed for a future migration. There is no silent auto-equip or value loss.

If any step fails, the new copy is discarded, the original remains active under its last supported ruleset when possible, and the slot is marked migration_blocked. Other slots and settings remain usable.

10. Local, Cloud, And Multi-Device Conflict

Cloud is an optional transport of complete validated commits. It does not merge inventories, rewards, route nodes, or operation journals. A cloud upload carries campaign_id, lineage_id, commit_id, parent_commit_id, and sequence.

Relationship Resolution
Same commit No action
Local is ancestor of cloud Offer cloud as newer; retain local backup
Cloud is ancestor of local Keep local; queue upload
Different lineage/campaign Offer to import cloud into an empty/duplicated slot
Same lineage, divergent descendants Never auto-merge; show both boundaries and require keep local, keep cloud, or duplicate one

Conflict choices use commit boundary, chapter, play duration, Anchor, and last known objective. Wall-clock newer is display context only. The default is Cancel and inspect, and no option deletes the unchosen copy until the selected copy has been validated and backed up.

11. Clock Rollback And Offline Behavior

All gameplay clocks are simulation ticks or monotonic session duration. Wall clock is used only for human-readable save dates and diagnostics.

On startup:

wall_delta = current_wall_clock - last_seen_wall_clock
if wall_delta < -300 seconds or wall_delta > 180 days:
    record clock_anomaly for diagnostics
    do not change gameplay state or deny play

Changing the device clock cannot grant or remove Crowns, Supplies, shop offers, route nodes, XP, Mastery, unlocks, or platform achievements. A queued platform event with a timestamp outside service limits remains derived from committed facts and is retried without a gameplay reward.

While the game is closed, expedition and encounter time advances by exactly zero ticks. On return, the player receives a compact recap of the saved state, not an offline income panel. Optional platform uploads are bounded background work and stop cleanly without blocking local commits.

12. Failure State Model

Failure is classified before consequences are calculated:

Failure kind Predicate Earliest committed boundary Outcome
Encounter defeat All deployed units Lost/Downed with no legal rescue, objective fails, or Fracture reaches 180 s outcome_locked Apply difficulty defeat transaction
Voluntary battle retreat Three-second retreat channel completes after its legal time outcome_locked Apply retreat transaction
Route withdrawal Player confirms at noncombat node Route commit Bank declared fraction, end expedition
Hazard failure Authored visible condition fails Node result Apply declared Supply/Strain/Cache delta once
Invalid action State/target/cost precondition fails No commit Spend nothing; return one reason
Process interruption App is suspended/killed/crashes Last valid save point Resume/reconstruct; not a gameplay failure
Save corruption Active commit fails validation Last valid backup Recover/quarantine; not a gameplay failure

13. Defeat, Retreat, Strain, And Cache Consequences

13.1 Cache Loss Algorithm

Every Unsecured Cache line stores cache_line_id, stable reward ID, quantity, risk_value, and acceptance sequence. Before node commitment, the preview shows the exact loss set for the active difficulty.

For loss fraction P:

loss_target = floor(total_cache_risk_value * P)
order cache lines by acceptance_sequence descending, then cache_line_id
remove/split lines until removed risk value >= loss_target
never remove a banked line or more than the Unsecured Cache owns

Crowns and stackable materials may split exactly. An indivisible item that would overshoot remains in the Cache and the algorithm continues to the next line; if no line can satisfy the remainder, the actual loss is smaller and is shown. There is no random loss roll.

13.2 Difficulty Transactions

Contract Defeat Battle retreat Noncombat withdrawal
Wayfinder Lose 25% Cache, consume up to 1 Supply, +1 Strain to each Downed/Lost unit (at most two affected units) Lose 15% Cache, consume 1 Supply, +1 Strain to units Lost before channel Bank 75% Cache, discard remainder, end expedition
Stormbound Lose 50% Cache, consume up to 2 Supplies, +1 Strain to every Downed/Lost deployed unit Lose 30% Cache, consume 2 Supplies, +1 Strain to Lost units Bank 50% Cache, discard remainder, end expedition
Iron Oath Lose 100% Cache, consume all Supplies, +2 Strain to Lost units, close current route Lose 60% Cache, consume all but 1 Supply, +1 Strain to all deployed units Bank 25% Cache, discard remainder, end expedition

After the transaction, if at least one Supply remains and at least one deployed or reserve adventurer has fewer than three Strain, the party returns to the last Anchor with the revealed graph intact except where Iron Oath closes the current route. Otherwise the expedition ends at the Guild. Banked state, chapter restorations, roster, inventory, XP, and Mastery remain intact.

For a noncombat withdrawal, resolve the Cache bank/discard fraction first, then apply the unused-Supplies Crown conversion from the economy authority in the same expedition-close operation. Defeat never receives that conversion.

On victory, an adventurer whose Rescue Window expired gains one Strain as stated in the systems authority. A rescued adventurer gains none from that Downed event. Strain caps at three; overflow is discarded and logged. At three Strain the adventurer cannot deploy but can still be inspected, respeced, equipped, and recovered. One Supply at a Camp/Anchor clears one Strain from one adventurer; returning to the Guild clears all Strain without cost and ends the expedition.

13.3 Recovery Choice

The Recovery state shows:

  1. outcome cause and first preventable event;
  2. exact Cache lines lost, banked, and retained;
  3. each adventurer's Strain before/after;
  4. Supplies before/after and route position;
  5. three legal next actions: reconfigure at Anchor, train against the same seed with no reward, or end the expedition at the Guild.

It never offers a paid continue, random reroll, automatic best-loadout button, or destructive character replacement. The same-seed training replay is not a rollback: it is a separate no-reward simulation whose result cannot alter the committed failure.

14. Confirmation, Delete, And Reset Boundaries

The following actions require a review state with exact effects, then a separate confirm input: difficulty increase, Reweave-law activation, route withdrawal, Cache discard, salvage of an unlocked item, cloud conflict resolution, migration fallback, campaign-slot overwrite, and campaign-slot delete.

Deleting a campaign slot requires selecting the exact slot name and confirming again after the summary. The active save moves to a recoverable local trash area for seven successful launches or until the player explicitly empties it. A profile-wide reset additionally requires entering a displayed localized phrase; controller and touch users can select the phrase from a two-step dialog rather than type it. No cancel/default focus may point at delete.

An accepted confirm receives an operation ID. Repeating the input returns the first result and cannot delete or spend twice.

15. Accessibility, Localization, And Input Requirements

  • Save state is communicated by text and icon, never only color or a spinning indicator. Saving, Saved, Recovered, Conflict, and Blocked have distinct accessible labels.
  • Closing during Saving is legal; the previous active commit remains valid.
  • Every recovery/conflict dialog supports 130% UI scale, screen-reader order, keyboard/controller focus, and a complete tap-select path.
  • Technical IDs are hidden by default but available in an inspectable details panel and copyable/exportable diagnostic manifest.
  • Localized messages use semantic IDs and typed values. They never concatenate amounts, item names, operation kinds, or recovery boundaries into fragments.
  • Numeric loss previews use localized formatting and also list affected cache rows, so a percentage is not the only explanation.
  • Haptics and audio may confirm a commit but cannot be the only saved/failed cue.
  • A controller disconnect, focus loss, or input-device change pauses target selection and spends nothing until a new confirm is accepted.

16. Required Recovery And Failure Test Matrix

Test Interruption/fault point Expected invariant
Equip commit After cost validation, before atomic pointer switch Old loadout or complete new loadout; never partial
Purchase duplicate Same operation ID delivered twice One cost and one item
Directive duplicate Same command ID at same tick twice One charge and one event
Battle suspend During cast at tick 417 Resume at tick 417 boundary; identical final hash
Battle crash After snapshot write, before pointer update Use prior valid snapshot and journal; identical final hash
Outcome crash After victory predicate, before report animation Reopen same victory report and fixed rewards
Reward crash After card choice, before screen transition Exactly one Cache/bank line; same card on reload
Anchor crash During Cache banking Entire Cache banked once or still unsecured; no split duplicate
Clock rollback Device clock moves back 24 hours Diagnostic only; zero gameplay change
Seven-day offline Close mid-expedition and return Zero tick/resource progress; recap and resume
Active save corrupt Break checksum Load newest valid backup, quarantine active, show boundary
All battle snapshots corrupt Valid committed start and command journal remain Restart identical seed/loadout, replay commands at original ticks, no cost/reward change
Migration failure Invalid stable-ID mapping Original stays active; new copy discarded; export offered
Cloud divergence Local and cloud share parent but differ No merge/overwrite; keep/duplicate choice
Wayfinder defeat 200 Cache risk value, two Lost units, 4 Supplies Lose 50 value, spend 1 Supply, each gets 1 Strain
Stormbound withdrawal 200 Cache at noncombat node Bank 100 value, discard 100, end expedition
Iron Oath defeat Any positive Cache and Supplies Lose all Cache/Supplies, route closes, roster remains

17. Acceptance Checklist

  • Schema, ruleset, content, lineage, commit, and operation versions are separated and migration paths are copy-first.
  • Every resource/action/reward transaction is atomic and idempotent.
  • Battle suspension and crash recovery reproduce the authoritative hash.
  • Duplicate input, reward claim, platform retry, and cloud conflict cannot duplicate or erase gameplay state.
  • Active, backup, Anchor, pre-migration, and quarantine behavior is explicit.
  • Clock rollback and bounded offline behavior create zero gameplay progress.
  • Wayfinder, Stormbound, and Iron Oath failure consequences match the economy authority and never delete a first-campaign adventurer or banked asset.
  • Recovery, conflict, overwrite, reset, and delete flows meet input, accessibility, localization, and confirmation requirements.