b6c6ddfbc4
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
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""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()
|