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:
+37
-1
@@ -14,6 +14,13 @@ extends Control
|
||||
@onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton
|
||||
@onready var empty_label: Label = $VBoxContainer/EmptyLabel
|
||||
|
||||
## Cached so each world card can decide whether to show "Edit World"
|
||||
## (docs/architecture/world_persistence_impl.md §5.3: chunk writes require
|
||||
## `world.owner_id == caller`, same as `update_world`) without an extra
|
||||
## round trip per card. -1 until resolved / on failure, in which case no
|
||||
## card shows the edit entry point.
|
||||
var _current_user_id: int = -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
refresh_button.disabled = false
|
||||
@@ -22,6 +29,11 @@ func _ready() -> void:
|
||||
error_label.visible = false
|
||||
empty_label.visible = false
|
||||
world_list.visible = false
|
||||
|
||||
var me_result: Dictionary = await ApiClient.get_me()
|
||||
if me_result.ok and me_result.data is Dictionary:
|
||||
_current_user_id = int((me_result.data as Dictionary).get("id", -1))
|
||||
|
||||
_refresh_worlds()
|
||||
|
||||
|
||||
@@ -146,10 +158,26 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
||||
enter_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
enter_button.pressed.connect(_on_enter_world_pressed.bind(world_id, enter_button))
|
||||
|
||||
var buttons_row := HBoxContainer.new()
|
||||
buttons_row.add_child(enter_button)
|
||||
|
||||
# "Edit World" (world_persistence_impl.md §6 D4): a non-joined entry
|
||||
# point into the in-game editor -- it does NOT call POST /join. Only
|
||||
# shown for the world's owner, matching the write-side authorization the
|
||||
# save path itself enforces (§5.3), so a non-owner never sees a button
|
||||
# that would just 403 on the first save.
|
||||
var owner_id := int(world_dict.get("owner_id", -1))
|
||||
if _current_user_id != -1 and owner_id == _current_user_id:
|
||||
var edit_button := Button.new()
|
||||
edit_button.text = "Edit World"
|
||||
edit_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
edit_button.pressed.connect(_on_edit_world_pressed.bind(world_id))
|
||||
buttons_row.add_child(edit_button)
|
||||
|
||||
card.add_child(name_label)
|
||||
card.add_child(desc_label)
|
||||
card.add_child(meta_label)
|
||||
card.add_child(enter_button)
|
||||
card.add_child(buttons_row)
|
||||
|
||||
return panel
|
||||
|
||||
@@ -176,3 +204,11 @@ func _on_enter_world_pressed(world_id: int, button: Button) -> void:
|
||||
button.text = "Enter World"
|
||||
error_label.text = "Couldn't join: %s" % result.detail
|
||||
error_label.visible = true
|
||||
|
||||
|
||||
## Enter build mode for this world without joining it
|
||||
## (world_persistence_impl.md §6 D4). No /join call, no ENet connection, no
|
||||
## dedicated server -- game.gd loads/saves the world's chunks over HTTP.
|
||||
func _on_edit_world_pressed(world_id: int) -> void:
|
||||
Session.begin_edit(world_id)
|
||||
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||
|
||||
Reference in New Issue
Block a user