Implements docs/architecture/world_persistence_impl.md §9's backend task:
chunk/room/revision Postgres schema (migration 0005), a Python port of the
world.md §3.6 binary chunk codec with golden-file tests, a WorldStore
service (world.md §5.1 minus flush_dirty/rebuild_index), the public
world-content router (rooms/chunks CRUD, optimistic concurrency, batch
writes) and the internal read-only API for dedicated servers, and
game_servers.py changes: a per-spawn bearer token, --api-url/--server-token
args, and a real "ready"-field wait replacing _await_port, which bound the
UDP port before content existed and failed open (no error) past its
deadline.
create_world now auto-creates the "main" room.
Deviations/findings, not silently decided:
- chunk_codec.py implements real IEEE-754 float32 packing rather than
mirroring a confirmed bug in scripts/chunk_data_decoder.gd's
_read_float32/_write_float32, which do a lossy int<->float cast instead
of a bit reinterpretation (verified against Godot 4.6.2 headless) and
will corrupt any non-integer position/rotation/scale/bounds value. Not
fixed here (client-side, out of scope); flagged for the client task.
- world_persistence_impl.md references a world's monotonic
content_revision (§6/§7.2/§8.1) but never defines where it's stored.
Resolved as MAX(id) over the append-only world_chunk_revisions table,
scoped to world_id, rather than adding an undocumented column.
- No existing Godot-side golden fixture/test vectors were found to reuse,
so tests/fixtures/chunks/*.bin are authored fresh from this
implementation (see generate_fixtures.py).
Verification: 132/132 backend pytest tests pass against a real ephemeral
Postgres (both reusing an already-running instance and cold via
conftest.py's own docker-compose bootstrap), including migration
upgrade/downgrade/upgrade, codec round-trip + validation-error tests, and
full HTTP round trips for PUT/GET/batch/delete/revisions/rollback and the
internal API's token auth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
New avatar_loadouts table (one row per user, loadout stored as a single
JSONB document matching the client's AvatarLoadout.to_json() shape) and
GET/PUT /api/avatar/loadout.
The server has no copy of the asset catalog, so it validates only the
envelope (JSON object, non-empty base_body, object slots, size cap) --
which-asset-in-which-slot validation stays client-side. New accounts get a
server default (base_human_male_01, no slots) with is_default:true so the
client can tell "never customised" from "customised back to base".
- models/avatar_loadout.py + 0004 migration + User.avatar_loadout relation
- routers/avatar.py wired into main.py
- tests/test_avatar_loadout.py (7 tests: default, save/read, replace,
validation, auth) -- full suite 68 passed
- lifted the duplicated _override_db fixture from 3 test files into
conftest.py (new test files need it too)
- docs/architecture/avatar.md §9a: the persistence design
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
- main_menu.gd: Play button now navigates to world_list.tscn
- world_list.gd: fix invalid theme_override_constants property-path syntax
(use add_theme_constant_override), fix @onready node paths missing
/HeaderHBox/, cast Variant dict values to String to avoid type-inference
errors when cards actually render
- world_list.tscn: wire up Back/Refresh button signal connections (scene
had zero [connection] blocks, buttons were dead)
- backend: fix real bug where World.visibility enum sent member names
("PUBLIC") to Postgres instead of lowercase values, breaking both GET
and POST /api/worlds/ for any real query
- backend: fix WorldResponse.created_at typed as str instead of datetime,
causing a second 500 on GET once the enum bug was fixed
- add smoke_test_world_list.gd: headless scene-level smoke test that
registers a user, instantiates the real scene, and confirms world cards
render from live backend data (not just an API-level check)