Editor save path: single/batch chunk PUT, 409 retry, non-joined Edit World entry

Implements world_persistence_impl.md §9 client/editor task items 2-3:

- ChunkSaveService (scripts/editor/chunk_save_service.gd): single-chunk
  PUT with base_revision, batch PUT for multi-chunk edits, and a real
  409 -> re-fetch -> retry loop. Full-chunk overwrite (§8.2 D5) means
  "re-apply the edit" is just retrying with the locally-edited payload
  at the freshly-learned revision, not delta replay.
- InGameEditor gets a Save button (+ Ctrl+S) that surfaces live_session
  as an explicit warning instead of ignoring it, per §6 D4.
- "Edit World" entry point on the world list (owner-gated) that calls
  Session.begin_edit() instead of ApiClient.join_world() -- editing
  never joins, opens no ENet connection, spawns no dedicated server.
- game.gd wires Session.editing to fetch the real (0,0,0) chunk over
  HTTP and open the editor against it, falling back to an empty chunk
  for an unwritten coordinate.

Verified against a real backend (register/world/save/reload, a genuine
409 from two racing saves, atomic 2-chunk batch, live_session field) --
new scripts/smoke_test_editor_save.gd (19/19) plus an extended
smoke_test_world_list.gd covering the non-joined entry point (11/11).
Full existing Godot smoke suite (18 files) and backend pytest (132)
still pass unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
This commit is contained in:
2026-09-14 23:35:33 +00:00
parent 31f6974c7a
commit 4ff74ae823
7 changed files with 742 additions and 9 deletions
+21
View File
@@ -6,11 +6,19 @@ extends Node
## POST /api/worlds/{id}/join; game.gd reads it in _ready() to decide whether
## to run networked or fall back to local single-player (e.g. game.tscn opened
## directly, or the backend has no orchestration).
##
## world_list.gd also calls begin_edit() for the "Edit world" entry point
## (docs/architecture/world_persistence_impl.md §6 D4): editing is
## deliberately **non-joined** -- it never calls POST /join, so `active`
## stays false and game.gd never opens an ENet connection for it. `editing`
## is the separate signal game.gd checks to open the in-game editor against
## a real, HTTP-fetched chunk instead of running gameplay.
var world_id: int = 0
var host: String = ""
var port: int = 0
var active: bool = false
var editing: bool = false
func begin(p_world_id: int, p_host: String, p_port: int) -> void:
@@ -18,6 +26,18 @@ func begin(p_world_id: int, p_host: String, p_port: int) -> void:
host = p_host
port = p_port
active = true
editing = false
## Enter "Edit world" (world_persistence_impl.md §6 D4): non-joined, no
## ENet connection, no dedicated server. game.gd loads/saves chunks over
## HTTP instead.
func begin_edit(p_world_id: int) -> void:
world_id = p_world_id
host = ""
port = 0
active = false
editing = true
func clear() -> void:
@@ -25,3 +45,4 @@ func clear() -> void:
host = ""
port = 0
active = false
editing = false