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
Implements the in-game editor's shared, data-native core and a
Control-based UI on top of it, per docs/architecture/editor.md §7.1/§7.2,
scoped to 2D per §8's own risk mitigation (the default world is flat,
CHUNK_SIZE_Z=1 -- no 3D gizmos here).
EditorCore (scripts/editor/):
- ChunkEditorModel wraps a ChunkDataDecoder.ChunkPayload directly (no
parallel data model) and adds a TileLayerData.set_tile() mutator
alongside the existing get_tile().
- EditorUndoRedoStack wraps Godot's built-in UndoRedo (game-usable, not
the editor-only EditorUndoRedoManager) so every mutation is a real,
revertible do/undo Callable pair over ChunkEditorModel's raw setters.
- SelectionManager tracks the selected object id as UI state, deliberately
outside the undo/redo stack.
- ToolRegistry + 5 tools (BrushTool, ObjectPlaceTool, SelectTool,
EraseTool, TransformTool -- the 5th covers move/rotate/scale, which the
doc's tool list doesn't literally name but the task requires) is the
single choke point every mutation goes through.
- EditorObjectCatalog (assets/editor/object_catalog.json) is a new,
small "placeable object" catalog -- AvatarCatalog and SpawnCatalog were
checked first and are both scoped to other concerns, so this mirrors
their loading convention rather than reusing either.
InGameEditor UI (scripts/editor/ui/): EditorViewport (2D top-down tile
grid + object dots), EditorToolbar, PalettePanel, InspectorPanel, wired
together in InGameEditor. Built from Control nodes in code (matching
game.gd's existing dynamic-UI convention) rather than a hand-authored
.tscn.
Entry point: game.gd gates build mode behind a new toggle_build_mode
input action (F2), freezing the player and opening a fresh empty
ChunkEditorModel. Loading a *real* chunk from the backend for editing,
and saving edits back, are explicitly out of scope -- a sibling task
owns the batch-PUT/conflict-handling save path
(world_persistence_impl.md).
Verification: scripts/smoke_test_editor_core.gd decodes a real backend
fixture (backend/tests/fixtures/chunks/simple_tile_chunk.bin), drives
place/move/rotate/scale/delete/paint/erase entirely through
EditorCore.use_tool()/ToolRegistry (never touching ChunkEditorModel's
raw setters directly), undoes and redoes every step, and round-trips the
result through encode_binary()/decode_binary() to prove the edited model
stays a valid, byte-compatible ChunkPayload. 69/69 assertions pass. Full
existing Godot smoke suite (16 other scripts/smoke_test_*.gd, backend +
dedicated-server-dependent ones included) still passes with no
regressions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD