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:
@@ -48,6 +48,11 @@ func _run() -> void:
|
||||
|
||||
var api := root.get_node("ApiClient")
|
||||
assert(api != null, "ApiClient autoload not found")
|
||||
# Bare autoload identifiers (e.g. `Session`) don't resolve when a script
|
||||
# is compiled for a headless `godot --script ...` run -- same note as
|
||||
# HttpChunkSource._api_client() / ChunkSaveService._api_client().
|
||||
var session := root.get_node("Session")
|
||||
assert(session != null, "Session autoload not found")
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
@@ -150,6 +155,32 @@ func _run() -> void:
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 3: "Edit World" entry point (world_persistence_impl.md
|
||||
# §6 D4) -- shown for the seeded world's owner (this test's own
|
||||
# user), and, critically, wired to Session.begin_edit() rather than
|
||||
# ApiClient.join_world() -- pressing it must never call /join.
|
||||
print("\n=== Test 3: Edit World entry point (owner-gated, non-joined) ===")
|
||||
var owned_buttons := _find_buttons_in_first_card(world_list_node)
|
||||
var edit_button: Button = null
|
||||
for b in owned_buttons:
|
||||
if b.text == "Edit World":
|
||||
edit_button = b
|
||||
if _check("owner's card shows an Edit World button", edit_button != null):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if edit_button != null:
|
||||
session.clear()
|
||||
edit_button.pressed.emit()
|
||||
var editing_set: bool = session.editing and not session.active and session.world_id == int((create_result.data as Dictionary)["id"])
|
||||
if _check("pressing Edit World sets Session.editing (non-joined) for the right world, not Session.active", editing_set):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
print(" Session: editing=%s active=%s world_id=%s" % [session.editing, session.active, session.world_id])
|
||||
session.clear()
|
||||
|
||||
# --- Summary ---
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
if failed > 0:
|
||||
@@ -159,6 +190,22 @@ func _run() -> void:
|
||||
quit(0)
|
||||
|
||||
|
||||
## Depth-first collect every Button under the first rendered world card.
|
||||
func _find_buttons_in_first_card(world_list_node: VBoxContainer) -> Array:
|
||||
var out: Array = []
|
||||
if world_list_node.get_child_count() == 0:
|
||||
return out
|
||||
_collect_buttons(world_list_node.get_child(0), out)
|
||||
return out
|
||||
|
||||
|
||||
func _collect_buttons(node: Node, out: Array) -> void:
|
||||
if node is Button:
|
||||
out.append(node)
|
||||
for c in node.get_children():
|
||||
_collect_buttons(c, out)
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> bool:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
|
||||
Reference in New Issue
Block a user