[Game Systems] Versioned stat schemas + stats storage + validation service #139
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation/Context
The closest existing structured surface for NPC/creature/PC data is
LoreEntry.sidebar_fields— a JSONB list of{label, value, visibility, …}rows wherevalueis a string capped at 300 chars (LoreSidebarFieldInput,webapp/backend/app/routers/campaigns.py:179-202). That's fine for loose infobox rowsbut too weak to represent a real stat block: no ints, no lists, no nesting, and typed
vs. free-form rows would collide in one list. This issue introduces a versioned,
declarative stat schema per game system and a place to store validated structured stats
against it, so later work (the stat-block editor, issue 5; the #130 upgrade, issue 6;
the conversion wizard, issue 7; Foundry's
npc_to_actor, issue 8) has a real targetinstead of parsing markdown or overloading
sidebar_fields.Three storage options were weighed in the investigation report (§3.2): a typed variant
of
sidebar_fields(too weak — no typing/nesting, collides with free-form rows), adedicated
stat_blockstable (cleanest relational integrity but duplicates theversioning/draft-rail/export story that
lore_entriesalready has), and a JSONBstatsenvelope column on
lore_entries(matches house style —sidebar_fieldsandtimeline_eventsare already JSONB-on-the-entry,models/lore_entry.py:219-230— andrides along with version snapshots, export/import, and the draft rail for free).
Recommendation: the JSONB envelope (Option C), with the dedicated-table approach
documented as the escalation path if cross-entry stat queries become a real need.
Approach
New table
game_system_schemas:definitionis a constrained, JSON-Schema-flavoured document, one section perapplicable
LoreType(npc,creature,player_character): field groups, field keys,labels, types (
int,str,text,enum,list[str],list[text],boolfor v1),and constraints (
min/max/choices). Schema definitions live as JSON files in therepo (
webapp/backend/app/game_systems/dnd5e/schema_v1.json,pf2e/schema_v1.json,covering
npc/creaturekinds only for v1) and are loaded/seeded into the table bymigration — versioned in git, reviewable in PRs. Published versions are immutable;
changes mean a new version row. A stat envelope records the version it was written
against and is never silently migrated forward.
statsenvelope columns:The envelope stores the system key (not a UUID) so it's stable across exports and
survives a campaign unlinking/relinking (§4.4 of the report). Default visibility is
GM-only, matching the #130 decision on generated stat content. No DB-level FK from
stats.systemto the registry — validated on write in the service layer instead.Validator service —
webapp/backend/app/services/game_system_service.py: validatesa
valuespayload against a(system, version)pair (~150 lines; use thejsonschemalibrary if preferred over hand-rolled checks, no other new dependency needed). Wire
validation into the lore entry CRUD endpoints so a
statswrite is rejected if itdoesn't match the current schema for the entry's linked system.
Migration (raw-SQL seed, same house rules as #133):
CREATE TABLE game_system_schemaswithstatusasTEXT+CHECK(nosa.Enuminop.create_table), seed dnd5e v1 and pf2e v1 rows loaded from the in-repo JSON files,then
ALTER TABLE lore_entries ADD COLUMN stats JSONB NULLandALTER TABLE lore_entry_versions ADD COLUMN stats JSONB NULL.Dependencies
game_system_schemas.system_idFKsto
game_systems, and the campaign needs a linked system for its lore entries' statsto validate against.
Out of scope
generate_statblock(the #130 upgrade) to emit schema-validvalues— issue 6.statsfrom existing free-form data —issue 7.
this design (validation only, per the report's "NOT rules" principle).
(
created_byongame_systems) but authoring tooling is out of scope here.Acceptance criteria
game_system_schemastable exists, seeded with dnd5e v1 and pf2e v1 definitionscovering
npcandcreatureentity kinds, loaded from versioned JSON files in therepo.
lore_entries.statsandlore_entry_versions.statsare nullable JSONB columns;existing entries are unaffected (
stats IS NULL).stats.valuespayload that doesn't conform to thereferenced
(system, schema_version)definition (wrong type, out-of-range, unknownenum choice) and accepts a conforming one.
statsthrough the lore endpoints validates server-side;reading returns the envelope as-is including its self-described
systemandschema_version.LoreEntryVersionsnapshots capturestatsalongsidesidebar_fieldson everyapproval/edit that changes it.
stats.visibilityis GM-only unless explicitly set otherwise.Done and verified — merged into the integration branch via PR #206.
Verification (Docker, py3.12):
test_stat_schemas.py), no regressions.c7d8e9f0a1b2against a real Postgres 16:upgrade head→downgrade -1→upgrade headclean;game_system_schemasseeded with dnd5e v1 / pf2e v1 (active);statscolumns added tolore_entriesandlore_entry_versions.Delivered:
game_system_schemastable + versioned JSON schema definitions (dnd5e/pf2e, npc+creature); validatedstatsJSONB envelope on lore entries + version snapshots; hand-rolled validator (server-stamps system/version, rejects unknown fields / type mismatches / out-of-range / bad enums); lore CRUD wiring (ValueError→400, GM-only visibility filtering);GET /api/game-systems/{system_id}/schemafor #140;ensure_builtin_schemasself-heal for thecreate_alltest harness.Follow-up flagged (out of scope here): campaign export/import does not yet carry
stats, so a stat block is lost on export→re-import. I'll track this — likely folded into a later issue with an export-schema-version bump rather than retrofitted here.Closing; ships to
mainwith the v3.9.0 release.statsenvelope through campaign export/import #214