feat(gameserver): persisted mana + Magery on per-entity save serialization (#36) #57

Merged
marco merged 1 commit from feat/spell-mana-foundation into main 2026-07-19 06:13:06 +02:00
Owner

Spell-system slice 0 (foundation) — part of #36 — plus the persistence rework it required.

Gameplay

  • Mana pool: Mobile.Mana (current) with MaxMana = INT, regenerating one point every ManaRegenTicks (~1/s) via a new World-owned SpellSystem component stepped from the tick.
  • Magery skill: added to the enum + catalog, seeded on every player (no gain trigger yet — that arrives with the first cast in slice 1).
  • Vitals are now persisted: current HP + mana survive relog (were reset to full before). A dead logout returns at full HP — ghost state isn't persisted.

Persistence rework (ModernUO-style per-entity serialization)

Driven by the design discussion on #36: instead of an intermediate DTO spread across ~7 files, each entity serializes itself.

  • PlayerMobile.Serialize/Deserialize (position + vitals + skills) and Item.Serialize/Deserialize (identity + slot), each starting with its own version int — evolve a format by bumping that version and branching in its own Deserialize, never a central switch.
  • A player's owned sub-graph (player + items) is one self-contained blob (PlayerSerializer). WorldSaveFile is now a container mapping player id → blob; PlayerStore an id → blob map. Blobs are built on the sim thread (World.SerializePlayer/SerializeOnlinePlayers) and flushed off-thread.
  • Deleted the intermediate DTOs (PlayerPersistState, PlayerRecord) and the granular per-aspect store methods.
  • Follows ModernUO's per-entity Serialize/Deserialize; diverges from its global serial-reference graph + two-pass linker — blobs are per-player sub-graphs (no cross-player references yet), so no global linker until trading/world-containers need one (container nesting extends the blob + a per-blob link pass).
  • Pre-production: no migration — an unrecognized save file loads empty.

Verification

just lint → build ok, 0 warnings. just test → full suite green (GameServer 125, Shared 51, Client.Core 46, Auth 12, Assets 4, AssetExtractor 7).

Definition of Done (delta beyond the base DoD)

  • A player's current HP and mana persist across disconnect/reconnect (round-trip tested at the World level and through the WebSocket integration tests).
  • Mana regenerates toward INT at the configured cadence and never overflows (unit-tested).
  • Every player is seeded with the Magery skill.
  • Persistence uses per-entity Serialize/Deserialize with per-entity versions; no intermediate persistence DTO remains.
  • Owned items round-trip through a player's blob (backpack + equipped, re-arming the weapon on restore).
  • An unrecognized/old save file loads empty instead of crashing.

Design docs updated: spell-system.md (mana persisted, not transient) and the CLAUDE.md persistence section (per-entity model). Epic #36 rewritten to the agreed design with the slice plan.

Spell-system **slice 0** (foundation) — part of #36 — plus the persistence rework it required. ## Gameplay - **Mana pool**: `Mobile.Mana` (current) with `MaxMana = INT`, regenerating one point every `ManaRegenTicks` (~1/s) via a new `World`-owned **`SpellSystem`** component stepped from the tick. - **Magery skill**: added to the enum + catalog, seeded on every player (no gain trigger yet — that arrives with the first cast in slice 1). - **Vitals are now persisted**: current HP + mana survive relog (were reset to full before). A dead logout returns at full HP — ghost state isn't persisted. ## Persistence rework (ModernUO-style per-entity serialization) Driven by the design discussion on #36: instead of an intermediate DTO spread across ~7 files, **each entity serializes itself**. - `PlayerMobile.Serialize/Deserialize` (position + vitals + skills) and `Item.Serialize/Deserialize` (identity + slot), each starting with its **own version int** — evolve a format by bumping that version and branching in its own `Deserialize`, never a central switch. - A player's owned sub-graph (player + items) is one self-contained **blob** (`PlayerSerializer`). `WorldSaveFile` is now a container mapping player id → blob; `PlayerStore` an id → blob map. Blobs are built on the sim thread (`World.SerializePlayer`/`SerializeOnlinePlayers`) and flushed off-thread. - Deleted the intermediate DTOs (`PlayerPersistState`, `PlayerRecord`) and the granular per-aspect store methods. - **Follows** ModernUO's per-entity `Serialize`/`Deserialize`; **diverges** from its global serial-reference graph + two-pass linker — blobs are per-player sub-graphs (no cross-player references yet), so no global linker until trading/world-containers need one (container nesting extends the blob + a per-blob link pass). - **Pre-production**: no migration — an unrecognized save file loads empty. ## Verification `just lint` → build ok, **0 warnings**. `just test` → full suite green (GameServer 125, Shared 51, Client.Core 46, Auth 12, Assets 4, AssetExtractor 7). ## Definition of Done (delta beyond the base DoD) - [x] A player's current HP **and** mana persist across disconnect/reconnect (round-trip tested at the `World` level and through the WebSocket integration tests). - [x] Mana regenerates toward `INT` at the configured cadence and never overflows (unit-tested). - [x] Every player is seeded with the `Magery` skill. - [x] Persistence uses per-entity `Serialize`/`Deserialize` with per-entity versions; no intermediate persistence DTO remains. - [x] Owned items round-trip through a player's blob (backpack + equipped, re-arming the weapon on restore). - [x] An unrecognized/old save file loads empty instead of crashing. Design docs updated: `spell-system.md` (mana persisted, not transient) and the CLAUDE.md persistence section (per-entity model). Epic #36 rewritten to the agreed design with the slice plan.
feat(gameserver): persisted mana + Magery on per-entity save serialization
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m51s
2bf4e2319f
Spell-system foundation (slice 0), plus the persistence rework it needed.

Gameplay:
- Add a current mana pool (Mobile.Mana, cap = MaxMana = INT) that regenerates
  toward max each tick via a new World-owned SpellSystem component.
- Add the Magery skill (enum + catalog), seeded on every player.
- Current vitals (HP + mana) are now authoritative, persisted state that
  survives relog; a dead logout returns at full HP (ghost state isn't persisted).

Persistence rework (ModernUO-style per-entity serialization):
- Each entity serializes itself: PlayerMobile.Serialize/Deserialize (position +
  vitals + skills) and Item.Serialize/Deserialize (identity + slot), each with its
  own version int. Evolve a format by bumping that entity's version — no central
  switch, no intermediate DTOs (PlayerPersistState / PlayerRecord deleted).
- A player's owned sub-graph (player + items) is one self-contained blob
  (PlayerSerializer); WorldSaveFile is a container mapping id -> blob; PlayerStore
  is an id -> blob map. Blobs are built on the sim thread, flushed off-thread.
- Follows ModernUO's per-entity model; diverges from its global serial-reference
  graph + two-pass linker — blobs are per-player sub-graphs, so no global linker
  until cross-player references (trading, world containers) need one.
- Pre-production: no migration; an unrecognized save file loads empty.

SaveWriter/SaveReader wrap the binary stream with domain-friendly helpers.
marco merged commit f6fefb4e9c into main 2026-07-19 06:13:06 +02:00
marco deleted branch feat/spell-mana-foundation 2026-07-19 06:13:06 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!57
No description provided.