Wire client chunk streaming + dedicated-server content load to the backend
Implements the client-side half of docs/architecture/world_persistence_impl.md
(the backend API in backend/app/routers/{world_content,internal}.py and
game_servers.py's --api-url/--server-token/_await_ready were already merged):
- scripts/http_chunk_source.gd: new HttpChunkSource adapter satisfying
ChunkStreamManager.set_server_rpc()'s seam. Fetches chunks over HTTP from
the public world_content API (JWT via ApiClient), with ETag/If-None-Match
(304) support backed by a user://chunk_cache/<checksum> cache, and a
bounds-query list_chunks() helper (§8.1's chunk index). A 404 (unwritten
coordinate) renders empty instead of erroring, per §8.1.
- scripts/server.gd: startup now parses --api-url/--server-token, and
before binding the ENet port fetches the world's manifest + the
spawn-neighbourhood chunks from /api/internal (§7.2/§7.3/§8.3), decodes
them, and builds real StaticBody3D collision from solid tiles + non-
walkable objects. Falls back to test_level.tscn if the backend is
unreachable, the world has no rooms, or the room has zero chunks -- never
hangs or crashes. Writes "ready"/"content" (plus diagnostic
chunks_loaded/collision_shapes counts) into the status file that
game_servers.py's _await_ready() already reads.
- scripts/api_client.gd: exposes a `base_url` getter so other client code
building its own requests (HttpChunkSource; server.gd doesn't use this
autoload at all, since its auth model is the per-spawn server token, not
a player JWT) can target the same backend.
Also fixes three real, previously-dormant bugs this work's first real
exercise of these code paths uncovered (nothing before this called
encode_binary() or fed real data through set_server_rpc()):
- chunk_data_decoder.gd: `_build_metadata_section` never wrapped its
output with the section id/length header every other section builder
uses, so `encode_binary()` produced corrupt payloads the decoder read as
a single garbage-length section ("truncated section body").
- chunk_stream_manager.gd: `_parse_coord_from_key` assigned a
PackedStringArray (String.split()'s real return type) to an
Array[String]-typed variable -- a hard crash.
- chunk_stream_manager.gd: `_instantiate_chunk_scene_with_data` read a
nonexistent `data.format_version` field; ChunkPayload's field is
`version`.
Adds scripts/smoke_test_http_chunk_source.gd, a real end-to-end smoke test
(register, create world, PUT a real chunk, stream it back through
ChunkStreamManager + HttpChunkSource, plus the unwritten-coordinate and
304-refetch paths).
Verified against a real backend + Postgres:
- Seeded real chunk content, joined through the real
GameServerSupervisor spawn path: status file reports
content=loaded, chunks_loaded=1, collision_shapes=33.
- A room with zero chunks and an unreachable backend (closed port) both
fall back to test_level.tscn cleanly (content=fallback, ready=true, no
hang/crash).
- Full existing Godot smoke suite (16 suites, including
smoke_test_enter_world.gd and smoke_test_network_client.gd) passes with
no regressions.
- Backend pytest (132/132) untouched and still green -- no backend/
changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
This commit is contained in:
@@ -241,7 +241,11 @@ func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder.
|
||||
chunk_scene.coord = coord
|
||||
chunk_scene.key = data.key
|
||||
chunk_scene.bounds = data.bounds
|
||||
chunk_scene.format_version = data.format_version
|
||||
# ChunkDataDecoder.ChunkPayload's field is named `version`, not
|
||||
# `format_version` -- this was a hard crash ("Invalid access to
|
||||
# property or key 'format_version'") the first time real decoded data
|
||||
# ever reached this dormant method (via HttpChunkSource).
|
||||
chunk_scene.format_version = data.version
|
||||
chunk_scene.revision = data.revision
|
||||
|
||||
# Add tiles
|
||||
@@ -348,7 +352,13 @@ func clear_all_chunks() -> void:
|
||||
|
||||
func _parse_coord_from_key(key: String) -> Variant:
|
||||
"""Parse chunk coordinate from a canonical chunk key."""
|
||||
var parts: Array[String] = key.split(":")
|
||||
# String.split() returns PackedStringArray, not Array[String] -- those
|
||||
# are different container types with no implicit conversion, so this
|
||||
# raised "Trying to assign a value of type 'PackedStringArray' to a
|
||||
# variable of type 'Array[String]'" the moment real data reached this
|
||||
# dormant method for the first time (via HttpChunkSource's
|
||||
# _on_chunk_data_received call -- nothing exercised this path before).
|
||||
var parts: PackedStringArray = key.split(":")
|
||||
if parts.size() < 5:
|
||||
return null
|
||||
return Vector3i(
|
||||
|
||||
Reference in New Issue
Block a user