Fix build stamp visibility, add create world UI, and add Save as New World to editor
checkpoint / checkpoint (push) Successful in 5m54s

This commit is contained in:
2026-09-16 14:21:48 +00:00
parent 73172b1e28
commit 41d1f7438c
6 changed files with 397 additions and 19 deletions
+194 -4
View File
@@ -33,8 +33,11 @@ var _inspector: InspectorPanel
var _viewport: EditorViewport
var _close_button: Button
var _save_button: Button
var _save_as_button: Button
var _save_status_label: Label
var _instruction_label: Label
var _save_as_modal: Control = null
## Save-path state (world_persistence_impl.md §8.1/§8.2), set via
## `setup_persistence()`. `_save_service == null` means there is nothing to
@@ -103,11 +106,17 @@ func _build_ui() -> void:
_save_button = Button.new()
_save_button.text = "Save (Ctrl+S)"
_save_button.disabled = (_save_service == null)
_save_button.tooltip_text = "Save changes to server (Ctrl+S)" if _save_service != null else "Saving not available in scratch mode"
_save_button.tooltip_text = "Save changes to server (Ctrl+S)"
_save_button.pressed.connect(_on_save_pressed)
top_bar.add_child(_save_button)
_save_as_button = Button.new()
_save_as_button.text = "Save as New World"
_save_as_button.tooltip_text = "Save this creation as a new world you own on the server"
_save_as_button.pressed.connect(_on_save_as_pressed)
top_bar.add_child(_save_as_button)
_close_button = Button.new()
_close_button.text = "Close (F2 / Esc)"
_close_button.tooltip_text = "Exit build mode and return to game"
@@ -285,8 +294,13 @@ func _on_close_pressed() -> void:
## (§8.2/§5.4) -- this just reflects the outcome and, per §6 D4, surfaces
## the `live_session` flag as a warning instead of ignoring it.
func _on_save_pressed() -> void:
if _save_service == null or _saving:
if _saving:
return
if _save_service == null:
_set_save_status("No world selected to save to. Choose 'Save as New World'.", Color(1.0, 0.8, 0.25))
_show_save_as_modal()
return
_saving = true
_save_button.disabled = true
_set_save_status("Saving…", Color(0.85, 0.85, 0.85))
@@ -307,7 +321,174 @@ func _on_save_pressed() -> void:
else:
_set_save_status(msg, Color(0.55, 1.0, 0.55))
else:
_set_save_status("Save failed: %s" % str(result.get("detail", "unknown error")), Color(1.0, 0.45, 0.45))
var status_code := int(result.get("status", 0))
var detail := str(result.get("detail", "unknown error"))
if status_code == 403 or "owner" in detail.to_lower():
_set_save_status("Save failed: You do not own world %s. Use 'Save as New World' to save your work." % _world_id, Color(1.0, 0.45, 0.45))
_show_save_as_modal()
else:
_set_save_status("Save failed: %s" % detail, Color(1.0, 0.45, 0.45))
func _on_save_as_pressed() -> void:
_show_save_as_modal()
func _show_save_as_modal() -> void:
if _save_as_modal != null and is_instance_valid(_save_as_modal):
_save_as_modal.queue_free()
var overlay := ColorRect.new()
overlay.name = "SaveAsOverlay"
overlay.color = Color(0, 0, 0, 0.75)
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
overlay.add_child(center)
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(460, 0)
var style := StyleBoxFlat.new()
style.bg_color = Color(0.18, 0.18, 0.22, 1.0)
style.border_color = Color(0.4, 0.6, 0.9, 1.0)
style.set_border_width_all(2)
style.set_corner_radius_all(8)
style.content_margin_left = 24
style.content_margin_right = 24
style.content_margin_top = 20
style.content_margin_bottom = 20
panel.add_theme_stylebox_override("panel", style)
center.add_child(panel)
var form := VBoxContainer.new()
form.add_theme_constant_override("separation", 12)
panel.add_child(form)
var title := Label.new()
title.text = "Save as New World"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 20)
form.add_child(title)
var hint := Label.new()
hint.text = "Creates a new world owned by you and saves your current build into it."
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
hint.add_theme_font_size_override("font_size", 13)
hint.add_theme_color_override("font_color", Color(0.75, 0.75, 0.75))
form.add_child(hint)
var name_lbl := Label.new()
name_lbl.text = "World Name:"
name_lbl.add_theme_font_size_override("font_size", 14)
form.add_child(name_lbl)
var name_input := LineEdit.new()
name_input.placeholder_text = "Enter world name..."
name_input.text = "My Creation" if (_world_id.is_empty() or _world_id == "0") else "Remix of World %s" % _world_id
form.add_child(name_input)
var desc_lbl := Label.new()
desc_lbl.text = "Description:"
desc_lbl.add_theme_font_size_override("font_size", 14)
form.add_child(desc_lbl)
var desc_input := LineEdit.new()
desc_input.placeholder_text = "Describe your world..."
desc_input.text = "Created in editor"
form.add_child(desc_input)
var err_label := Label.new()
err_label.add_theme_font_size_override("font_size", 13)
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
err_label.visible = false
form.add_child(err_label)
var btn_row := HBoxContainer.new()
btn_row.alignment = BoxContainer.ALIGNMENT_END
btn_row.add_theme_constant_override("separation", 10)
form.add_child(btn_row)
var cancel_btn := Button.new()
cancel_btn.text = "Cancel"
cancel_btn.pressed.connect(func(): overlay.queue_free())
btn_row.add_child(cancel_btn)
var save_btn := Button.new()
save_btn.text = "Save New World"
btn_row.add_child(save_btn)
save_btn.pressed.connect(func() -> void:
var wname := name_input.text.strip_edges()
if wname.is_empty():
err_label.text = "World name cannot be empty."
err_label.visible = true
return
save_btn.disabled = true
cancel_btn.disabled = true
err_label.text = "Creating world..."
err_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
err_label.visible = true
var api := _api_client()
if api == null:
save_btn.disabled = false
cancel_btn.disabled = false
err_label.text = "API client not available."
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
return
var res: Dictionary = await api.create_world(wname, desc_input.text.strip_edges())
if not res.get("ok", false):
save_btn.disabled = false
cancel_btn.disabled = false
err_label.text = "Failed to create world: %s" % str(res.get("detail", "unknown error"))
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
return
var new_world: Dictionary = res.get("data", {})
var new_id: int = int(new_world.get("id", 0))
var new_id_str: String = str(new_id)
err_label.text = "Saving chunk data..."
if _save_service == null:
_save_service = ChunkSaveService.new()
add_child(_save_service)
_save_service.world_id = new_id_str
_save_service.room_slug = "main"
_world_id = new_id_str
_room_slug = "main"
_coord = Vector3i.ZERO
_base_revision = 0
var sess := _session()
if sess != null:
sess.world_id = new_id
core.model.payload.world_id = _world_id
core.model.payload.room_id = _room_slug
core.model.payload.coord = _coord
var chunk_res: Dictionary = await _save_service.save_chunk(core.model, _coord, 0)
if not chunk_res.get("ok", false):
save_btn.disabled = false
cancel_btn.disabled = false
err_label.text = "World created (ID: %d) but chunk save failed: %s" % [new_id, str(chunk_res.get("detail", "unknown error"))]
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
return
_base_revision = int(chunk_res.get("revision", 1))
_save_button.disabled = false
_save_button.tooltip_text = "Save chunk changes to backend (Ctrl+S)"
_set_save_status("Saved successfully as new world '%s' (ID: %d, revision %d)!" % [wname, new_id, _base_revision], Color(0.55, 1.0, 0.55))
overlay.queue_free()
)
_save_as_modal = overlay
add_child(overlay)
func _set_save_status(text: String, color: Color) -> void:
@@ -316,3 +497,12 @@ func _set_save_status(text: String, color: Color) -> void:
_save_status_label.text = text
_save_status_label.add_theme_color_override("font_color", color)
_save_status_label.visible = true
func _api_client() -> Node:
return get_node_or_null("/root/ApiClient")
func _session() -> Node:
return get_node_or_null("/root/Session")