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
- 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.
- 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.
- The latest valid local commit remains playable offline. Cloud and platform services are optional copies, never gameplay authority.
- Offline gameplay progress is exactly zero. There are no real-time rewards, construction clocks, energy recovery, expedition simulation, or daily claims.
- 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.
- Migration is copy-first and reversible. A new build never rewrites the only known-valid save.
- Recovery is deterministic and inspectable. When data cannot be trusted, the game restores the newest valid backup and identifies the discarded boundary.
- 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:
- Validate the current commit, legal state, cost, target, and intent hash.
- If the operation is already applied, return its recorded result.
- Build the complete next payload in memory; do not mutate the active payload.
- Add the pending operation and all cost/result deltas to that payload.
- Validate schema, content references, resource non-negativity, item ownership, roster uniqueness, route ancestry, and encounter hash.
- Serialize to a new file, write checksum, flush file and containing directory, then atomically replace the active pointer.
- Mark the operation applied in the same new commit or an immediate child
commit. On recovery, a
pendingoperation with an applied delta is finalized; one without a complete delta is discarded. - 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:
- validate snapshots newest to oldest;
- load the newest valid snapshot with a matching encounter and ruleset;
- replay journaled commands and deterministic streams to the last committed tick;
- compare the reconstructed state hash with the stored checkpoint hash;
- if it matches, resume and record
snapshot_recovered; - if no snapshot matches, roll back to
committedand 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:
- envelope parse and size bounds;
- CRC32 and SHA-256;
- schema and required fields;
- campaign/lineage/parent commit relationships;
- resource bounds and nonnegative balances;
- stable content IDs and item ownership uniqueness;
- roster and profession topology;
- expedition route ancestry and committed node;
- 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, orreport); - 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:
- Validate and preserve the original as
pre_migration. - Build a migration plan of adjacent version steps; skipping a step is illegal.
- Resolve stable content-ID mappings and list any legacy rows.
- Apply each pure migration function to a copy.
- Validate every invariant and recompute derived values from source fields.
- Write a new lineage child commit with migration IDs and old/new hashes.
- 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:
- outcome cause and first preventable event;
- exact Cache lines lost, banked, and retained;
- each adventurer's Strain before/after;
- Supplies before/after and route position;
- 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, andBlockedhave distinct accessible labels. - Closing during
Savingis 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.