Complete M11 Creator Studio and M12 Trust & Safety for PC5
checkpoint / checkpoint (push) Failing after 3h14m9s
checkpoint / checkpoint (push) Failing after 3h14m9s
- Creator Studio: Visual behavior & property inspector, GLB model importer, binary persistence - Trust & Safety: 18+ TOS confirmation (DOB removed), creator guidelines & briefing card on join, in-game reporting HUD - Test Harness: Add --filter, --skip-backend, --skip-godot to run_checkpoint_tests.py
This commit is contained in:
@@ -4,13 +4,26 @@ extends VBoxContainer
|
||||
## `PalettePanel`). Lists every id in an `EditorObjectCatalog` and emits
|
||||
## which one the player picked; `InGameEditor` remembers it as the pending
|
||||
## `catalog_id` for the next `ObjectPlaceTool` placement.
|
||||
##
|
||||
## Creator Studio feature: In-game .glb/.gltf 3D Model Importer.
|
||||
## Creators can import custom 3D models from disk, validate them via GLTFDocument,
|
||||
## and place them directly into their worlds.
|
||||
|
||||
signal object_selected(catalog_id: String)
|
||||
signal model_imported(catalog_id: String)
|
||||
|
||||
var _list: ItemList
|
||||
var _catalog: EditorObjectCatalog = null
|
||||
var _ids: Array[String] = []
|
||||
|
||||
var _btn_import_model: Button
|
||||
var _import_panel: VBoxContainer
|
||||
var _model_name_edit: LineEdit
|
||||
var _model_path_edit: LineEdit
|
||||
var _btn_browse: Button
|
||||
var _file_dialog: FileDialog
|
||||
var _import_status_label: Label
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "PalettePanel"
|
||||
@@ -33,10 +46,82 @@ func _ready() -> void:
|
||||
|
||||
_list = ItemList.new()
|
||||
_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_list.custom_minimum_size = Vector2(0, 180)
|
||||
_list.custom_minimum_size = Vector2(0, 160)
|
||||
_list.item_selected.connect(_on_item_selected)
|
||||
add_child(_list)
|
||||
|
||||
_setup_import_ui()
|
||||
|
||||
|
||||
func _setup_import_ui() -> void:
|
||||
_btn_import_model = Button.new()
|
||||
_btn_import_model.text = "+ Import .glb Model"
|
||||
_btn_import_model.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
||||
_btn_import_model.pressed.connect(_on_import_button_pressed)
|
||||
add_child(_btn_import_model)
|
||||
|
||||
_import_panel = VBoxContainer.new()
|
||||
_import_panel.visible = false
|
||||
add_child(_import_panel)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Import Custom Model (.glb)"
|
||||
title.add_theme_font_size_override("font_size", 12)
|
||||
title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
|
||||
_import_panel.add_child(title)
|
||||
|
||||
var name_row := HBoxContainer.new()
|
||||
var name_lbl := Label.new()
|
||||
name_lbl.text = "Name:"
|
||||
name_lbl.custom_minimum_size = Vector2(50, 0)
|
||||
name_row.add_child(name_lbl)
|
||||
_model_name_edit = LineEdit.new()
|
||||
_model_name_edit.placeholder_text = "e.g. Vintage Chair"
|
||||
_model_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_row.add_child(_model_name_edit)
|
||||
_import_panel.add_child(name_row)
|
||||
|
||||
var path_row := HBoxContainer.new()
|
||||
var path_lbl := Label.new()
|
||||
path_lbl.text = "Path:"
|
||||
path_lbl.custom_minimum_size = Vector2(50, 0)
|
||||
path_row.add_child(path_lbl)
|
||||
_model_path_edit = LineEdit.new()
|
||||
_model_path_edit.placeholder_text = "Path to .glb or .gltf"
|
||||
_model_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
path_row.add_child(_model_path_edit)
|
||||
|
||||
_btn_browse = Button.new()
|
||||
_btn_browse.text = "..."
|
||||
_btn_browse.pressed.connect(_on_browse_pressed)
|
||||
path_row.add_child(_btn_browse)
|
||||
_import_panel.add_child(path_row)
|
||||
|
||||
_import_status_label = Label.new()
|
||||
_import_status_label.text = ""
|
||||
_import_status_label.add_theme_font_size_override("font_size", 10)
|
||||
_import_panel.add_child(_import_status_label)
|
||||
|
||||
var btn_row := HBoxContainer.new()
|
||||
var do_import_btn := Button.new()
|
||||
do_import_btn.text = "Import"
|
||||
do_import_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
do_import_btn.pressed.connect(_on_do_import_pressed)
|
||||
btn_row.add_child(do_import_btn)
|
||||
|
||||
var cancel_btn := Button.new()
|
||||
cancel_btn.text = "Cancel"
|
||||
cancel_btn.pressed.connect(_on_cancel_import_pressed)
|
||||
btn_row.add_child(cancel_btn)
|
||||
_import_panel.add_child(btn_row)
|
||||
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_file_dialog.filters = ["*.glb ; GLTF Binary Files", "*.gltf ; GLTF Scene Files"]
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
|
||||
func set_catalog(catalog: EditorObjectCatalog) -> void:
|
||||
_catalog = catalog
|
||||
@@ -51,6 +136,8 @@ func _refresh() -> void:
|
||||
for id in _catalog.get_all_ids():
|
||||
var entry := _catalog.resolve(id)
|
||||
var label: String = entry.get("display_name", id)
|
||||
if entry.get("category", "") == "custom":
|
||||
label = "★ " + label
|
||||
_list.add_item(label)
|
||||
_ids.append(id)
|
||||
|
||||
@@ -66,3 +153,104 @@ func get_selected_catalog_id() -> String:
|
||||
if selected.is_empty():
|
||||
return ""
|
||||
return _ids[selected[0]]
|
||||
|
||||
|
||||
func _on_import_button_pressed() -> void:
|
||||
_import_panel.visible = true
|
||||
_btn_import_model.visible = false
|
||||
_import_status_label.text = ""
|
||||
|
||||
|
||||
func _on_cancel_import_pressed() -> void:
|
||||
_import_panel.visible = false
|
||||
_btn_import_model.visible = true
|
||||
|
||||
|
||||
func _on_browse_pressed() -> void:
|
||||
_file_dialog.popup_centered(Vector2i(600, 400))
|
||||
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
_model_path_edit.text = path
|
||||
if _model_name_edit.text.strip_edges().is_empty():
|
||||
_model_name_edit.text = path.get_file().get_basename().capitalize()
|
||||
|
||||
|
||||
func _on_do_import_pressed() -> void:
|
||||
var path := _model_path_edit.text.strip_edges()
|
||||
var display_name := _model_name_edit.text.strip_edges()
|
||||
if path.is_empty():
|
||||
_import_status_label.text = "Error: Please select a file path."
|
||||
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||
return
|
||||
if display_name.is_empty():
|
||||
display_name = path.get_file().get_basename().capitalize()
|
||||
|
||||
var cat_id := import_model_file(path, display_name)
|
||||
if cat_id.is_empty():
|
||||
_import_status_label.text = "Error: Failed to import/validate model."
|
||||
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||
return
|
||||
|
||||
_import_panel.visible = false
|
||||
_btn_import_model.visible = true
|
||||
_model_name_edit.text = ""
|
||||
_model_path_edit.text = ""
|
||||
|
||||
|
||||
## Import a 3D model (.glb/.gltf), validate it, copy to user custom assets,
|
||||
## and register it into the active EditorObjectCatalog.
|
||||
func import_model_file(file_path: String, display_name: String, category: String = "custom") -> String:
|
||||
if not FileAccess.file_exists(file_path):
|
||||
push_error("PalettePanel: File does not exist: %s" % file_path)
|
||||
return ""
|
||||
|
||||
# Validate model using GLTFDocument
|
||||
var gltf := GLTFDocument.new()
|
||||
var state := GLTFState.new()
|
||||
var err := gltf.append_from_file(file_path, state)
|
||||
if err != OK:
|
||||
push_error("PalettePanel: GLTFDocument failed to parse file (%d): %s" % [err, file_path])
|
||||
return ""
|
||||
|
||||
# Copy to user custom assets directory
|
||||
var dir := DirAccess.open("user://")
|
||||
if dir != null:
|
||||
if not dir.dir_exists("custom_assets"):
|
||||
dir.make_dir("custom_assets")
|
||||
|
||||
var filename := file_path.get_file()
|
||||
var target_path := "user://custom_assets/%s" % filename
|
||||
var copy_err := DirAccess.copy_absolute(file_path, target_path)
|
||||
if copy_err != OK:
|
||||
# If direct copy failed, use original path
|
||||
target_path = file_path
|
||||
|
||||
# Generate clean unique catalog id
|
||||
var clean_slug := display_name.to_lower().replace(" ", "_").replace("-", "_")
|
||||
var regex := RegEx.new()
|
||||
regex.compile("[^a-z0-9_]")
|
||||
clean_slug = regex.sub(clean_slug, "", true)
|
||||
if clean_slug.is_empty():
|
||||
clean_slug = "model"
|
||||
var cat_id := "custom_%s" % clean_slug
|
||||
|
||||
var catalog_entry: Dictionary = {
|
||||
"asset_id": target_path,
|
||||
"type": "prop",
|
||||
"display_name": display_name,
|
||||
"category": category
|
||||
}
|
||||
|
||||
if _catalog != null:
|
||||
_catalog.register_custom_object(cat_id, catalog_entry)
|
||||
_refresh()
|
||||
# Select the newly imported model
|
||||
for i in range(_ids.size()):
|
||||
if _ids[i] == cat_id:
|
||||
_list.select(i)
|
||||
object_selected.emit(cat_id)
|
||||
break
|
||||
|
||||
model_imported.emit(cat_id)
|
||||
return cat_id
|
||||
|
||||
Reference in New Issue
Block a user