b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
980 lines
34 KiB
GDScript
980 lines
34 KiB
GDScript
@tool
|
|
extends Node
|
|
class_name SceneTools
|
|
## Scene operation tools for MCP.
|
|
## Handles: create_scene, read_scene, add_node, remove_node, modify_node_property,
|
|
## rename_node, move_node, attach_script, detach_script, set_collision_shape,
|
|
## set_sprite_texture
|
|
|
|
const _SKIP_PROPS: Dictionary[String, bool] = {
|
|
"script": true, "owner": true, "scene_file_path": true,
|
|
"unique_name_in_owner": true, "editor_description": true,
|
|
}
|
|
|
|
var _editor_plugin: EditorPlugin = null
|
|
|
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
_editor_plugin = plugin
|
|
|
|
# =============================================================================
|
|
# Shared helpers
|
|
# =============================================================================
|
|
func _refresh_and_reload(scene_path: String) -> void:
|
|
_refresh_filesystem()
|
|
_reload_scene_in_editor(scene_path)
|
|
|
|
func _refresh_filesystem() -> void:
|
|
if _editor_plugin:
|
|
_editor_plugin.get_editor_interface().get_resource_filesystem().scan()
|
|
|
|
func _reload_scene_in_editor(scene_path: String) -> void:
|
|
if not _editor_plugin:
|
|
return
|
|
var ei = _editor_plugin.get_editor_interface()
|
|
var edited = ei.get_edited_scene_root()
|
|
if edited and edited.scene_file_path == scene_path:
|
|
ei.reload_scene_from_path(scene_path)
|
|
|
|
func _ensure_res_path(path: String) -> String:
|
|
if not path.begins_with("res://"):
|
|
return "res://" + path
|
|
return path
|
|
|
|
func _load_scene(scene_path: String) -> Array:
|
|
"""Returns [scene_root, error_dict]. If error_dict is not empty, scene_root is null."""
|
|
if not FileAccess.file_exists(scene_path):
|
|
return [null, {&"ok": false, &"error": "Scene does not exist: " + scene_path}]
|
|
|
|
var packed = load(scene_path) as PackedScene
|
|
if not packed:
|
|
return [null, {&"ok": false, &"error": "Failed to load scene: " + scene_path}]
|
|
|
|
var root = packed.instantiate()
|
|
if not root:
|
|
return [null, {&"ok": false, &"error": "Failed to instantiate scene"}]
|
|
|
|
return [root, {}]
|
|
|
|
func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
|
|
"""Pack and save a scene. Returns error dict or empty on success."""
|
|
var packed = PackedScene.new()
|
|
var pack_result = packed.pack(scene_root)
|
|
if pack_result != OK:
|
|
scene_root.queue_free()
|
|
return {&"ok": false, &"error": "Failed to pack scene: " + str(pack_result)}
|
|
|
|
var save_result = ResourceSaver.save(packed, scene_path)
|
|
scene_root.queue_free()
|
|
|
|
if save_result != OK:
|
|
return {&"ok": false, &"error": "Failed to save scene: " + str(save_result)}
|
|
|
|
_refresh_and_reload(scene_path)
|
|
return {}
|
|
|
|
func _find_node(scene_root: Node, node_path: String) -> Node:
|
|
if node_path == "." or node_path.is_empty():
|
|
return scene_root
|
|
return scene_root.get_node_or_null(node_path)
|
|
|
|
func _parse_value(value: Variant) -> Variant:
|
|
"""Convert dictionary-encoded types to Godot types."""
|
|
if value is Dictionary:
|
|
var t: String = value.get(&"type", "")
|
|
match t:
|
|
"Vector2": return Vector2(value.get(&"x", 0), value.get(&"y", 0))
|
|
"Vector3": return Vector3(value.get(&"x", 0), value.get(&"y", 0), value.get(&"z", 0))
|
|
"Color": return Color(value.get(&"r", 1), value.get(&"g", 1), value.get(&"b", 1), value.get(&"a", 1))
|
|
"Vector2i": return Vector2i(value.get(&"x", 0), value.get(&"y", 0))
|
|
"Vector3i": return Vector3i(value.get(&"x", 0), value.get(&"y", 0), value.get(&"z", 0))
|
|
"Rect2": return Rect2(value.get(&"x", 0), value.get(&"y", 0), value.get(&"width", 0), value.get(&"height", 0))
|
|
return value
|
|
|
|
func _set_node_properties(node: Node, properties: Dictionary) -> void:
|
|
for prop_name: String in properties:
|
|
var prop_value = _parse_value(properties[prop_name])
|
|
node.set(prop_name, prop_value)
|
|
|
|
func _serialize_value(value: Variant) -> Variant:
|
|
match typeof(value):
|
|
TYPE_VECTOR2: return {&"type": &"Vector2", &"x": value.x, &"y": value.y}
|
|
TYPE_VECTOR3: return {&"type": &"Vector3", &"x": value.x, &"y": value.y, &"z": value.z}
|
|
TYPE_COLOR: return {&"type": &"Color", &"r": value.r, &"g": value.g, &"b": value.b, &"a": value.a}
|
|
TYPE_VECTOR2I: return {&"type": &"Vector2i", &"x": value.x, &"y": value.y}
|
|
TYPE_VECTOR3I: return {&"type": &"Vector3i", &"x": value.x, &"y": value.y, &"z": value.z}
|
|
TYPE_RECT2: return {&"type": &"Rect2", &"x": value.position.x, &"y": value.position.y, &"width": value.size.x, &"height": value.size.y}
|
|
TYPE_OBJECT:
|
|
if value and value is Resource and value.resource_path:
|
|
return {&"type": &"Resource", &"path": value.resource_path}
|
|
return null
|
|
_: return value
|
|
|
|
# =============================================================================
|
|
# create_scene
|
|
# =============================================================================
|
|
func create_scene(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var root_node_name: String = str(args.get(&"root_node_name", "Node"))
|
|
var root_node_type: String = str(args.get(&"root_node_type", ""))
|
|
var nodes: Array = args.get(&"nodes", [])
|
|
var attach_script_path: String = str(args.get(&"attach_script", ""))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path' parameter"}
|
|
if root_node_type.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'root_node_type' parameter"}
|
|
if not scene_path.ends_with(".tscn"):
|
|
scene_path += ".tscn"
|
|
if FileAccess.file_exists(scene_path):
|
|
return {&"ok": false, &"error": "Scene already exists: " + scene_path}
|
|
if not ClassDB.class_exists(root_node_type):
|
|
return {&"ok": false, &"error": "Invalid root node type: " + root_node_type}
|
|
|
|
# Ensure parent directory
|
|
var dir_path := scene_path.get_base_dir()
|
|
if not DirAccess.dir_exists_absolute(dir_path):
|
|
DirAccess.make_dir_recursive_absolute(dir_path)
|
|
|
|
var root: Node = ClassDB.instantiate(root_node_type) as Node
|
|
if not root:
|
|
return {&"ok": false, &"error": "Failed to create root node of type: " + root_node_type}
|
|
root.name = root_node_name
|
|
|
|
if not attach_script_path.is_empty():
|
|
var script_res = load(attach_script_path)
|
|
if script_res:
|
|
root.set_script(script_res)
|
|
|
|
var node_count := 0
|
|
for node_data: Variant in nodes:
|
|
if typeof(node_data) == TYPE_DICTIONARY:
|
|
var created = _create_node_recursive(node_data, root, root)
|
|
if created:
|
|
node_count += _count_nodes(created)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"path": scene_path, &"root_type": root_node_type, &"child_count": node_count,
|
|
&"message": "Scene created at " + scene_path}
|
|
|
|
func _create_node_recursive(data: Dictionary, parent: Node, owner: Node) -> Node:
|
|
var n_name: String = str(data.get(&"name", "Node"))
|
|
var n_type: String = str(data.get(&"type", "Node"))
|
|
var n_script: String = str(data.get(&"script", ""))
|
|
var props: Dictionary = data.get(&"properties", {})
|
|
var children: Array = data.get(&"children", [])
|
|
|
|
if not ClassDB.class_exists(n_type):
|
|
return null
|
|
var node: Node = ClassDB.instantiate(n_type) as Node
|
|
if not node:
|
|
return null
|
|
|
|
node.name = n_name
|
|
_set_node_properties(node, props)
|
|
|
|
if not n_script.is_empty():
|
|
var s = load(n_script)
|
|
if s:
|
|
node.set_script(s)
|
|
|
|
parent.add_child(node)
|
|
node.owner = owner
|
|
|
|
for child_data: Variant in children:
|
|
if typeof(child_data) == TYPE_DICTIONARY:
|
|
_create_node_recursive(child_data, node, owner)
|
|
return node
|
|
|
|
func _count_nodes(node: Node) -> int:
|
|
var count := 1
|
|
for child: Node in node.get_children():
|
|
count += _count_nodes(child)
|
|
return count
|
|
|
|
# =============================================================================
|
|
# read_scene
|
|
# =============================================================================
|
|
func read_scene(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var include_properties: bool = args.get(&"include_properties", false)
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path' parameter"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var structure = _build_node_structure(root, include_properties)
|
|
root.queue_free()
|
|
|
|
return {&"ok": true, &"scene_path": scene_path, &"root": structure}
|
|
|
|
func _build_node_structure(node: Node, include_props: bool, path: String = ".") -> Dictionary:
|
|
const PROPERTIES: PackedStringArray = ["position", "rotation", "scale", "size", "offset", "visible",
|
|
"modulate", "z_index", "text", "collision_layer", "collision_mask", "mass"]
|
|
var data := {&"name": str(node.name), &"type": node.get_class(), &"path": path, &"children": []}
|
|
var script = node.get_script()
|
|
if script:
|
|
data[&"script"] = script.resource_path
|
|
|
|
if include_props:
|
|
var props := {}
|
|
for prop_name: String in PROPERTIES:
|
|
var val = node.get(prop_name)
|
|
if val != null:
|
|
props[prop_name] = _serialize_value(val)
|
|
if not props.is_empty():
|
|
data[&"properties"] = props
|
|
|
|
for child: Node in node.get_children():
|
|
var child_path = child.name if path == "." else path + "/" + child.name
|
|
data[&"children"].append(_build_node_structure(child, include_props, child_path))
|
|
return data
|
|
|
|
# =============================================================================
|
|
# add_node
|
|
# =============================================================================
|
|
func add_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_name: String = str(args.get(&"node_name", ""))
|
|
var node_type: String = str(args.get(&"node_type", "Node"))
|
|
var parent_path: String = str(args.get(&"parent_path", "."))
|
|
var properties: Dictionary = args.get(&"properties", {})
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_name.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'node_name'"}
|
|
if not ClassDB.class_exists(node_type):
|
|
return {&"ok": false, &"error": "Invalid node type: " + node_type}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var parent = _find_node(root, parent_path)
|
|
if not parent:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Parent node not found: " + parent_path}
|
|
|
|
var new_node: Node = ClassDB.instantiate(node_type) as Node
|
|
if not new_node:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Failed to create node of type: " + node_type}
|
|
|
|
new_node.name = node_name
|
|
_set_node_properties(new_node, properties)
|
|
parent.add_child(new_node)
|
|
new_node.owner = root
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"scene_path": scene_path, &"node_name": node_name, &"node_type": node_type,
|
|
&"message": "Added %s (%s) to scene" % [node_name, node_type]}
|
|
|
|
# =============================================================================
|
|
# remove_node
|
|
# =============================================================================
|
|
func remove_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", ""))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_path.strip_edges().is_empty() or node_path == ".":
|
|
return {&"ok": false, &"error": "Cannot remove root node"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = root.get_node_or_null(node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var n_name = target.name
|
|
var n_type = target.get_class()
|
|
target.get_parent().remove_child(target)
|
|
target.queue_free()
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"scene_path": scene_path, &"removed_node": node_path,
|
|
&"message": "Removed %s (%s)" % [n_name, n_type]}
|
|
|
|
# =============================================================================
|
|
# modify_node_property
|
|
# =============================================================================
|
|
func modify_node_property(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
var property_name: String = str(args.get(&"property_name", ""))
|
|
var value = args.get(&"value")
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if property_name.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'property_name'"}
|
|
if value == null:
|
|
return {&"ok": false, &"error": "Missing 'value'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
# Check property exists
|
|
var prop_exists := false
|
|
for prop: Dictionary in target.get_property_list():
|
|
if prop[&"name"] == property_name:
|
|
prop_exists = true
|
|
break
|
|
if not prop_exists:
|
|
var node_type = target.get_class()
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Property '%s' not found on %s (%s). Use get_node_properties to discover available properties." % [property_name, node_path, node_type]}
|
|
|
|
var parsed = _parse_value(value)
|
|
var old_value = target.get(property_name)
|
|
|
|
# Validate resource type compatibility
|
|
if old_value is Resource and not (parsed is Resource):
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Property '%s' expects a Resource. Use specialized tools (set_collision_shape, set_sprite_texture) instead." % property_name}
|
|
|
|
target.set(property_name, parsed)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"scene_path": scene_path, &"node_path": node_path,
|
|
&"property_name": property_name, &"old_value": str(old_value), &"new_value": str(parsed),
|
|
&"message": "Set %s.%s = %s" % [node_path, property_name, str(parsed)]}
|
|
|
|
# =============================================================================
|
|
# rename_node
|
|
# =============================================================================
|
|
func rename_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", ""))
|
|
var new_name: String = str(args.get(&"new_name", ""))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_path.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'node_path'"}
|
|
if new_name.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'new_name'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var old_name = target.name
|
|
target.name = new_name
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"old_name": str(old_name), &"new_name": new_name,
|
|
&"message": "Renamed '%s' to '%s'" % [old_name, new_name]}
|
|
|
|
# =============================================================================
|
|
# move_node
|
|
# =============================================================================
|
|
func move_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", ""))
|
|
var new_parent_path: String = str(args.get(&"new_parent_path", "."))
|
|
var sibling_index: int = int(args.get(&"sibling_index", -1))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_path.strip_edges().is_empty() or node_path == ".":
|
|
return {&"ok": false, &"error": "Cannot move root node"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = root.get_node_or_null(node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var new_parent = _find_node(root, new_parent_path)
|
|
if not new_parent:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "New parent not found: " + new_parent_path}
|
|
|
|
target.get_parent().remove_child(target)
|
|
new_parent.add_child(target)
|
|
target.owner = root
|
|
|
|
if sibling_index >= 0:
|
|
new_parent.move_child(target, mini(sibling_index, new_parent.get_child_count() - 1))
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"message": "Moved '%s' to '%s'" % [node_path, new_parent_path]}
|
|
|
|
# =============================================================================
|
|
# duplicate_node
|
|
# =============================================================================
|
|
func duplicate_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", ""))
|
|
var new_name: String = str(args.get(&"new_name", ""))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_path.strip_edges().is_empty() or node_path == ".":
|
|
return {&"ok": false, &"error": "Cannot duplicate root node"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = root.get_node_or_null(node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var parent = target.get_parent()
|
|
if not parent:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Cannot duplicate - no parent"}
|
|
|
|
var duplicate = target.duplicate()
|
|
|
|
if new_name.is_empty():
|
|
var base_name = target.name
|
|
var counter = 2
|
|
new_name = base_name + str(counter)
|
|
while parent.has_node(NodePath(new_name)):
|
|
counter += 1
|
|
new_name = base_name + str(counter)
|
|
|
|
duplicate.name = new_name
|
|
parent.add_child(duplicate)
|
|
|
|
_set_owner_recursive(duplicate, root)
|
|
|
|
var original_index = target.get_index()
|
|
parent.move_child(duplicate, original_index + 1)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"new_name": new_name,
|
|
&"message": "Duplicated '%s' as '%s'" % [node_path, new_name]}
|
|
|
|
|
|
func _set_owner_recursive(node: Node, owner: Node) -> void:
|
|
node.owner = owner
|
|
for child: Node in node.get_children():
|
|
_set_owner_recursive(child, owner)
|
|
|
|
|
|
# =============================================================================
|
|
# reorder_node - simpler function just for changing sibling order
|
|
# =============================================================================
|
|
func reorder_node(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", ""))
|
|
var new_index: int = int(args.get(&"new_index", -1))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if node_path.strip_edges().is_empty() or node_path == ".":
|
|
return {&"ok": false, &"error": "Cannot reorder root node"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = root.get_node_or_null(node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var parent = target.get_parent()
|
|
if not parent:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Cannot reorder - no parent"}
|
|
|
|
var old_index = target.get_index()
|
|
var max_index = parent.get_child_count() - 1
|
|
new_index = clampi(new_index, 0, max_index)
|
|
|
|
if old_index == new_index:
|
|
root.queue_free()
|
|
return {&"ok": true, &"message": "No change needed"}
|
|
|
|
parent.move_child(target, new_index)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"old_index": old_index, &"new_index": new_index,
|
|
&"message": "Moved '%s' from index %d to %d" % [node_path, old_index, new_index]}
|
|
|
|
|
|
# =============================================================================
|
|
# attach_script
|
|
# =============================================================================
|
|
func attach_script(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
var script_path: String = str(args.get(&"script_path", ""))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if script_path.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'script_path'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var script_res = load(script_path)
|
|
if not script_res:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Failed to load script: " + script_path}
|
|
|
|
target.set_script(script_res)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"message": "Attached %s to node '%s'" % [script_path, node_path]}
|
|
|
|
# =============================================================================
|
|
# detach_script
|
|
# =============================================================================
|
|
func detach_script(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
target.set_script(null)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"message": "Detached script from node '%s'" % node_path}
|
|
|
|
# =============================================================================
|
|
# set_collision_shape
|
|
# =============================================================================
|
|
func set_collision_shape(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
var shape_type: String = str(args.get(&"shape_type", ""))
|
|
var shape_params: Dictionary = args.get(&"shape_params", {})
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if shape_type.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'shape_type'"}
|
|
if not ClassDB.class_exists(shape_type):
|
|
return {&"ok": false, &"error": "Invalid shape type: " + shape_type}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var shape = ClassDB.instantiate(shape_type)
|
|
if not shape:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Failed to create shape: " + shape_type}
|
|
|
|
if shape_params.has(&"radius"):
|
|
shape.set("radius", float(shape_params[&"radius"]))
|
|
if shape_params.has(&"height"):
|
|
shape.set("height", float(shape_params[&"height"]))
|
|
if shape_params.has(&"size"):
|
|
var size_data = shape_params[&"size"]
|
|
if typeof(size_data) == TYPE_DICTIONARY:
|
|
if size_data.has(&"z"):
|
|
shape.set("size", Vector3(size_data.get(&"x", 1), size_data.get(&"y", 1), size_data.get(&"z", 1)))
|
|
else:
|
|
shape.set("size", Vector2(size_data.get(&"x", 1), size_data.get(&"y", 1)))
|
|
|
|
target.set("shape", shape)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"message": "Set %s on node '%s'" % [shape_type, node_path]}
|
|
|
|
# =============================================================================
|
|
# set_sprite_texture
|
|
# =============================================================================
|
|
func set_sprite_texture(args: Dictionary) -> Dictionary:
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
var texture_type: String = str(args.get(&"texture_type", ""))
|
|
var texture_params: Dictionary = args.get(&"texture_params", {})
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if texture_type.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'texture_type'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var texture: Texture2D = null
|
|
|
|
match texture_type:
|
|
"ImageTexture":
|
|
var tex_path: String = str(texture_params.get(&"path", ""))
|
|
if tex_path.is_empty():
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Missing 'path' in texture_params for ImageTexture"}
|
|
texture = load(tex_path)
|
|
if not texture:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Failed to load texture: " + tex_path}
|
|
|
|
"PlaceholderTexture2D":
|
|
texture = PlaceholderTexture2D.new()
|
|
var size_data = texture_params.get(&"size", {&"x": 64, &"y": 64})
|
|
if typeof(size_data) == TYPE_DICTIONARY:
|
|
texture.size = Vector2(size_data.get(&"x", 64), size_data.get(&"y", 64))
|
|
|
|
"GradientTexture2D":
|
|
texture = GradientTexture2D.new()
|
|
texture.width = int(texture_params.get(&"width", 64))
|
|
texture.height = int(texture_params.get(&"height", 64))
|
|
|
|
"NoiseTexture2D":
|
|
texture = NoiseTexture2D.new()
|
|
texture.width = int(texture_params.get(&"width", 64))
|
|
texture.height = int(texture_params.get(&"height", 64))
|
|
|
|
_:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Unknown texture type: " + texture_type}
|
|
|
|
target.set("texture", texture)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {&"ok": true, &"message": "Set %s texture on node '%s'" % [texture_type, node_path]}
|
|
|
|
# =============================================================================
|
|
# get_scene_hierarchy (for visualizer)
|
|
# =============================================================================
|
|
func get_scene_hierarchy(args: Dictionary) -> Dictionary:
|
|
"""Get the full scene hierarchy with node information for the visualizer."""
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var hierarchy = _build_hierarchy_recursive(root, ".")
|
|
root.queue_free()
|
|
|
|
return {&"ok": true, &"scene_path": scene_path, &"hierarchy": hierarchy}
|
|
|
|
func _build_hierarchy_recursive(node: Node, path: String) -> Dictionary:
|
|
"""Build node hierarchy with all info needed for visualizer."""
|
|
var data := {
|
|
&"name": str(node.name),
|
|
&"type": node.get_class(),
|
|
&"path": path,
|
|
&"children": [],
|
|
&"child_count": node.get_child_count()
|
|
}
|
|
|
|
var script = node.get_script()
|
|
if script:
|
|
data[&"script"] = script.resource_path
|
|
|
|
var parent = node.get_parent()
|
|
if parent:
|
|
data[&"index"] = node.get_index()
|
|
|
|
for i: int in range(node.get_child_count()):
|
|
var child = node.get_child(i)
|
|
var child_path = child.name if path == "." else path + "/" + child.name
|
|
data[&"children"].append(_build_hierarchy_recursive(child, child_path))
|
|
|
|
return data
|
|
|
|
# =============================================================================
|
|
# get_scene_node_properties (dynamic property fetching)
|
|
# =============================================================================
|
|
func get_scene_node_properties(args: Dictionary) -> Dictionary:
|
|
"""Get all properties of a specific node in a scene with their current values."""
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var node_type = target.get_class()
|
|
var properties: Array = []
|
|
var categories: Dictionary = {}
|
|
|
|
for prop: Dictionary in target.get_property_list():
|
|
var prop_name: String = prop[&"name"]
|
|
|
|
if prop_name.begins_with("_"):
|
|
continue
|
|
if _SKIP_PROPS.has(prop_name):
|
|
continue
|
|
|
|
var usage = prop.get(&"usage", 0)
|
|
if not (usage & PROPERTY_USAGE_EDITOR):
|
|
continue
|
|
|
|
var current_value = target.get(prop_name)
|
|
|
|
var prop_info := {
|
|
&"name": prop_name,
|
|
&"type": prop[&"type"],
|
|
&"type_name": _type_id_to_name(prop[&"type"]),
|
|
&"hint": prop.get(&"hint", 0),
|
|
&"hint_string": prop.get(&"hint_string", ""),
|
|
&"value": _serialize_value(current_value),
|
|
&"usage": usage
|
|
}
|
|
|
|
var category = _get_property_category(target, prop_name)
|
|
prop_info[&"category"] = category
|
|
|
|
if not categories.has(category):
|
|
categories[category] = []
|
|
categories[category].append(prop_info)
|
|
properties.append(prop_info)
|
|
|
|
var chain: Array = []
|
|
var cls: String = node_type
|
|
while cls != "":
|
|
chain.append(cls)
|
|
cls = ClassDB.get_parent_class(cls)
|
|
|
|
root.queue_free()
|
|
|
|
return {
|
|
&"ok": true,
|
|
&"scene_path": scene_path,
|
|
&"node_path": node_path,
|
|
&"node_type": node_type,
|
|
&"node_name": target.name,
|
|
&"inheritance_chain": chain,
|
|
&"properties": properties,
|
|
&"categories": categories,
|
|
&"property_count": properties.size()
|
|
}
|
|
|
|
func _type_id_to_name(type_id: int) -> String:
|
|
"""Convert Godot type ID to human-readable name."""
|
|
match type_id:
|
|
TYPE_NIL: return "null"
|
|
TYPE_BOOL: return "bool"
|
|
TYPE_INT: return "int"
|
|
TYPE_FLOAT: return "float"
|
|
TYPE_STRING: return "String"
|
|
TYPE_VECTOR2: return "Vector2"
|
|
TYPE_VECTOR2I: return "Vector2i"
|
|
TYPE_RECT2: return "Rect2"
|
|
TYPE_RECT2I: return "Rect2i"
|
|
TYPE_VECTOR3: return "Vector3"
|
|
TYPE_VECTOR3I: return "Vector3i"
|
|
TYPE_TRANSFORM2D: return "Transform2D"
|
|
TYPE_VECTOR4: return "Vector4"
|
|
TYPE_VECTOR4I: return "Vector4i"
|
|
TYPE_PLANE: return "Plane"
|
|
TYPE_QUATERNION: return "Quaternion"
|
|
TYPE_AABB: return "AABB"
|
|
TYPE_BASIS: return "Basis"
|
|
TYPE_TRANSFORM3D: return "Transform3D"
|
|
TYPE_PROJECTION: return "Projection"
|
|
TYPE_COLOR: return "Color"
|
|
TYPE_STRING_NAME: return "StringName"
|
|
TYPE_NODE_PATH: return "NodePath"
|
|
TYPE_RID: return "RID"
|
|
TYPE_OBJECT: return "Object"
|
|
TYPE_CALLABLE: return "Callable"
|
|
TYPE_SIGNAL: return "Signal"
|
|
TYPE_DICTIONARY: return "Dictionary"
|
|
TYPE_ARRAY: return "Array"
|
|
TYPE_PACKED_BYTE_ARRAY: return "PackedByteArray"
|
|
TYPE_PACKED_INT32_ARRAY: return "PackedInt32Array"
|
|
TYPE_PACKED_INT64_ARRAY: return "PackedInt64Array"
|
|
TYPE_PACKED_FLOAT32_ARRAY: return "PackedFloat32Array"
|
|
TYPE_PACKED_FLOAT64_ARRAY: return "PackedFloat64Array"
|
|
TYPE_PACKED_STRING_ARRAY: return "PackedStringArray"
|
|
TYPE_PACKED_VECTOR2_ARRAY: return "PackedVector2Array"
|
|
TYPE_PACKED_VECTOR3_ARRAY: return "PackedVector3Array"
|
|
TYPE_PACKED_COLOR_ARRAY: return "PackedColorArray"
|
|
_: return "Variant"
|
|
|
|
func _get_property_category(node: Node, prop_name: String) -> String:
|
|
"""Determine which class in the hierarchy defines this property."""
|
|
var cls: String = node.get_class()
|
|
while cls != "":
|
|
var class_props = ClassDB.class_get_property_list(cls, true)
|
|
for prop: Dictionary in class_props:
|
|
if prop[&"name"] == prop_name:
|
|
return cls
|
|
cls = ClassDB.get_parent_class(cls)
|
|
return node.get_class()
|
|
|
|
# =============================================================================
|
|
# set_scene_node_property (for visualizer inline editing)
|
|
# =============================================================================
|
|
func set_scene_node_property(args: Dictionary) -> Dictionary:
|
|
"""Set a property on a node in a scene (supports complex types)."""
|
|
var scene_path: String = _ensure_res_path(str(args.get(&"scene_path", "")))
|
|
var node_path: String = str(args.get(&"node_path", "."))
|
|
var property_name: String = str(args.get(&"property_name", ""))
|
|
var value = args.get(&"value")
|
|
var value_type: int = int(args.get(&"value_type", -1))
|
|
|
|
if scene_path.strip_edges() == "res://":
|
|
return {&"ok": false, &"error": "Missing 'scene_path'"}
|
|
if property_name.strip_edges().is_empty():
|
|
return {&"ok": false, &"error": "Missing 'property_name'"}
|
|
|
|
var result := _load_scene(scene_path)
|
|
if not result[1].is_empty():
|
|
return result[1]
|
|
|
|
var root: Node = result[0]
|
|
var target = _find_node(root, node_path)
|
|
if not target:
|
|
root.queue_free()
|
|
return {&"ok": false, &"error": "Node not found: " + node_path}
|
|
|
|
var parsed_value = _parse_typed_value(value, value_type)
|
|
var old_value = target.get(property_name)
|
|
|
|
target.set(property_name, parsed_value)
|
|
|
|
var err := _save_scene(root, scene_path)
|
|
if not err.is_empty():
|
|
return err
|
|
|
|
return {
|
|
&"ok": true,
|
|
&"scene_path": scene_path,
|
|
&"node_path": node_path,
|
|
&"property_name": property_name,
|
|
&"old_value": _serialize_value(old_value),
|
|
&"new_value": _serialize_value(parsed_value),
|
|
&"message": "Set %s.%s" % [node_path, property_name]
|
|
}
|
|
|
|
func _parse_typed_value(value, type_hint: int):
|
|
"""Parse a value based on its type hint."""
|
|
if type_hint == -1:
|
|
return _parse_value(value)
|
|
|
|
if typeof(value) == TYPE_DICTIONARY:
|
|
if value.has(&"type"):
|
|
return _parse_value(value)
|
|
|
|
match type_hint:
|
|
TYPE_VECTOR2:
|
|
return Vector2(value.get(&"x", 0), value.get(&"y", 0))
|
|
TYPE_VECTOR2I:
|
|
return Vector2i(value.get(&"x", 0), value.get(&"y", 0))
|
|
TYPE_VECTOR3:
|
|
return Vector3(value.get(&"x", 0), value.get(&"y", 0), value.get(&"z", 0))
|
|
TYPE_VECTOR3I:
|
|
return Vector3i(value.get(&"x", 0), value.get(&"y", 0), value.get(&"z", 0))
|
|
TYPE_COLOR:
|
|
return Color(value.get(&"r", 1), value.get(&"g", 1), value.get(&"b", 1), value.get(&"a", 1))
|
|
TYPE_RECT2:
|
|
return Rect2(value.get(&"x", 0), value.get(&"y", 0), value.get(&"width", 0), value.get(&"height", 0))
|
|
|
|
return value
|