Add world content persistence backend (Postgres chunks, internal API, game-server token auth)
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
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
"""Generates the golden-file chunk fixtures in this directory.
|
||||
|
||||
These pin the world.md §3.6 binary format so the Python codec
|
||||
(app/services/chunk_codec.py) and any future Godot-side test fixture can be
|
||||
checked for drift (world_persistence_impl.md §5.5's "drift control"
|
||||
requirement). No Godot-side golden fixture existed at the time this was
|
||||
written (searched scripts/ and tests/ for AGPC / binary-format test
|
||||
vectors -- none found), so these are authored here, from this
|
||||
implementation, as the first golden files for the format. The client/editor
|
||||
follow-on task should decode these same files with
|
||||
`ChunkDataDecoder.decode_binary()` and assert the same logical values
|
||||
(see the note in chunk_codec.py about a confirmed float32 handling bug in
|
||||
that decoder that will need fixing for full byte parity on non-integer
|
||||
transforms).
|
||||
|
||||
Run with: `python tests/fixtures/chunks/generate_fixtures.py` from `backend/`.
|
||||
Fixtures are checked in; this script is for regenerating them if the format
|
||||
or these examples change, not run as part of the test suite.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[3])) # backend/
|
||||
|
||||
from app.services import chunk_codec as cc
|
||||
|
||||
FIXTURES_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def _empty_chunk() -> cc.ChunkPayload:
|
||||
return cc.ChunkPayload(
|
||||
world_id="7", room_id="main", coord=(0, 0, 0),
|
||||
bounds=cc.ChunkBounds(min=(0.0, 0.0, 0.0), size=(32.0, 32.0, 1.0)),
|
||||
tile_layers=[], objects=[],
|
||||
flags=cc.ContentFlags(),
|
||||
created_at=1_700_000_000, updated_at=1_700_000_000, revision=1, author="fixture-gen",
|
||||
)
|
||||
|
||||
|
||||
def _simple_tile_chunk() -> cc.ChunkPayload:
|
||||
layer = cc.TileLayer(
|
||||
name="ground", layer_id=0, width=4, height=4, depth=1,
|
||||
palette=[
|
||||
cc.TileDef(id=0, asset_id="", solid=False, flags=0, variants=1),
|
||||
cc.TileDef(id=1, asset_id="tiles/grass", solid=False, flags=1, variants=4),
|
||||
cc.TileDef(id=2, asset_id="tiles/stone", solid=True, flags=2, variants=2),
|
||||
],
|
||||
tiles=[0, 1, 1, 0, 1, 2, 2, 1, 1, 2, 2, 1, 0, 1, 1, 0],
|
||||
)
|
||||
return cc.ChunkPayload(
|
||||
world_id="12", room_id="main", coord=(3, -1, 0),
|
||||
bounds=cc.ChunkBounds(min=(96.0, -32.0, 0.0), size=(32.0, 32.0, 1.0)),
|
||||
tile_layers=[layer], objects=[],
|
||||
flags=cc.ContentFlags(violence=0, mature=False, sources=[]),
|
||||
created_at=1_700_000_000, updated_at=1_700_000_500, revision=2, author="fixture-gen",
|
||||
)
|
||||
|
||||
|
||||
def _chunk_with_objects() -> cc.ChunkPayload:
|
||||
layer = cc.TileLayer(
|
||||
name="ground", layer_id=0, width=2, height=2, depth=1,
|
||||
palette=[cc.TileDef(id=0, asset_id="", solid=False, flags=0, variants=1)],
|
||||
tiles=[0, 0, 0, 0],
|
||||
)
|
||||
obj1 = cc.WorldObject(
|
||||
id="obj_0001", type="interactable", asset_id="props/chest",
|
||||
pos=(101.5, -110.25, 0.5), rot=(0.0, 0.0, 0.0, 1.0), scale=(1.0, 1.0, 1.0),
|
||||
dynamic=False,
|
||||
properties={"locked": False, "loot_table": "chest_basic"},
|
||||
flags=cc.ContentFlags(violence=0, mature=False, sources=[]),
|
||||
)
|
||||
obj2 = cc.WorldObject(
|
||||
id="obj_0002", type="spawn_point", asset_id="",
|
||||
pos=(96.0, -128.0, 1.0), rot=(0.0, 0.7071, 0.0, 0.7071), scale=(1.0, 1.0, 1.0),
|
||||
dynamic=True,
|
||||
properties={},
|
||||
flags=cc.ContentFlags(violence=1, mature=False, sources=["asset:props/chest"]),
|
||||
)
|
||||
return cc.ChunkPayload(
|
||||
world_id="12", room_id="main", coord=(3, -4, 0),
|
||||
bounds=cc.ChunkBounds(min=(96.0, -128.0, 0.0), size=(32.0, 32.0, 1.0)),
|
||||
tile_layers=[layer], objects=[obj1, obj2],
|
||||
flags=cc.ContentFlags(violence=1, mature=False, sources=["asset:props/chest"]),
|
||||
created_at=1_700_000_000, updated_at=1_700_001_000, revision=5, author="agent-bot",
|
||||
)
|
||||
|
||||
|
||||
FIXTURES = {
|
||||
"empty_chunk.bin": _empty_chunk,
|
||||
"simple_tile_chunk.bin": _simple_tile_chunk,
|
||||
"chunk_with_objects.bin": _chunk_with_objects,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for filename, builder in FIXTURES.items():
|
||||
payload = builder()
|
||||
data = cc.encode_binary(payload)
|
||||
(FIXTURES_DIR / filename).write_bytes(data)
|
||||
print(f"wrote {filename} ({len(data)} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user