Files
tactical-shooter/addons/godot_mcp/tools/visualizer_tools.gd
T
shawn b0c83af092 Fresh start: replace with naxIO/netfox-cs-sample foundation
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>
2026-07-02 20:55:20 -04:00

1024 lines
32 KiB
GDScript

@tool
extends Node
class_name VisualizerTools
## Crawls a Godot project and parses all GDScript files to build a project map.
var _editor_plugin: EditorPlugin = null
var _scene_tools_ref: Node = null
func set_editor_plugin(plugin: EditorPlugin) -> void:
_editor_plugin = plugin
func set_scene_tools_ref(scene_tools: Node) -> void:
_scene_tools_ref = scene_tools
func map_project(args: Dictionary) -> Dictionary:
"""Crawl the entire project and build a structural map of all scripts."""
var root_path: String = str(args.get(&"root", "res://"))
var include_addons: bool = bool(args.get(&"include_addons", false))
if not root_path.begins_with("res://"):
root_path = "res://" + root_path
# Collect all .gd files
var script_paths: Array = []
_collect_scripts(root_path, script_paths, include_addons)
if script_paths.is_empty():
return {&"ok": false, &"error": "No GDScript files found in " + root_path}
# Parse each script
var nodes: Array = []
var class_map: Dictionary = {} # class_name -> path
for path: String in script_paths:
var info: Dictionary = _parse_script(path)
nodes.append(info)
if info.get(&"class_name", "") != "":
class_map[info[&"class_name"]] = path
# Build edges
var edges: Array = []
for node: Dictionary in nodes:
var from_path: String = node[&"path"]
# extends relationship (resolve class_name to path)
var extends_class: String = node.get(&"extends", "")
if extends_class in class_map:
edges.append({&"from": from_path, &"to": class_map[extends_class], &"type": "extends"})
# preload/load references
for ref: String in node.get(&"preloads", []):
if ref.ends_with(".gd"):
edges.append({&"from": from_path, &"to": ref, &"type": "preload"})
# signal connections
for conn: Dictionary in node.get(&"connections", []):
var target: String = conn.get(&"target", "")
if target in class_map:
edges.append({&"from": from_path, &"to": class_map[target], &"type": "signal", &"signal_name": conn.get(&"signal", "")})
return {
&"ok": true,
&"project_map": {
&"nodes": nodes,
&"edges": edges,
&"total_scripts": nodes.size(),
&"total_connections": edges.size()
}
}
func _collect_scripts(path: String, results: Array, include_addons: bool) -> void:
"""Recursively collect all .gd files."""
var dir := DirAccess.open(path)
if dir == null:
return
dir.list_dir_begin()
var name := dir.get_next()
while name != "":
if name.begins_with("."):
name = dir.get_next()
continue
var full_path := path.path_join(name)
if dir.current_is_dir():
if name == "addons" and not include_addons:
name = dir.get_next()
continue
_collect_scripts(full_path, results, include_addons)
elif name.ends_with(".gd"):
results.append(full_path)
name = dir.get_next()
dir.list_dir_end()
func _parse_script(path: String) -> Dictionary:
"""Parse a GDScript file and extract its structure."""
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return {&"path": path, &"error": "Cannot open file"}
var content: String = file.get_as_text()
file.close()
var lines: PackedStringArray = content.split("\n")
var line_count: int = lines.size()
var description := ""
var extends_class := ""
var class_name_str := ""
var variables: Array = []
var functions: Array = []
var signals_list: Array = []
var preloads: Array = []
var connections: Array = []
# Regex patterns
var re_desc := RegEx.new()
re_desc.compile("^##\\s*@desc:\\s*(.+)")
var re_extends := RegEx.new()
re_extends.compile("^extends\\s+(\\w+)")
var re_class_name := RegEx.new()
re_class_name.compile("^class_name\\s+(\\w+)")
# Match: @export var name: Type = value OR var name: Type OR var name = value
# Captures: 1=@export, 2=@onready, 3=name, 4=type, 5=default
var re_var := RegEx.new()
re_var.compile("^(@export(?:\\([^)]*\\))?\\s+)?(@onready\\s+)?var\\s+(\\w+)\\s*(?::\\s*(\\w+))?(?:\\s*=\\s*(.+))?")
# Match: func name(params) -> ReturnType:
var re_func := RegEx.new()
re_func.compile("^func\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*(?:->\\s*(\\w+))?")
# Match: signal name(params)
var re_signal := RegEx.new()
re_signal.compile("^signal\\s+(\\w+)(?:\\(([^)]*)\\))?")
var re_preload := RegEx.new()
re_preload.compile("(?:preload|load)\\s*\\(\\s*\"(res://[^\"]+)\"\\s*\\)")
# Match: obj.signal.connect(...) pattern (Godot 4 style)
var re_connect_obj := RegEx.new()
re_connect_obj.compile("(\\w+)\\.(\\w+)\\.connect\\s*\\(")
# Match: signal.connect(...) pattern (direct signal)
var re_connect_direct := RegEx.new()
re_connect_direct.compile("^\\s*(\\w+)\\.connect\\s*\\(")
# Map of variable names to their types (for resolving signal connections)
var var_type_map: Dictionary = {}
# First pass: extract metadata and find function boundaries
var func_starts: Array = [] # [{line_idx, name}]
for i: int in range(line_count):
var line: String = lines[i]
var stripped: String = line.strip_edges()
# Description tag (check first 15 lines)
if i < 15 and description.is_empty():
var m := re_desc.search(stripped)
if m:
description = m.get_string(1)
continue
# extends
if extends_class.is_empty():
var m := re_extends.search(stripped)
if m:
extends_class = m.get_string(1)
continue
# class_name
if class_name_str.is_empty():
var m := re_class_name.search(stripped)
if m:
class_name_str = m.get_string(1)
continue
# Variables - only match top-level (not indented)
# Skip lines that start with tab or spaces (inside functions)
if not line.begins_with("\t") and not line.begins_with(" "):
var m_var := re_var.search(stripped)
if m_var:
var exported: bool = m_var.get_string(1).strip_edges() != ""
var onready: bool = m_var.get_string(2).strip_edges() != ""
var var_name: String = m_var.get_string(3)
var var_type: String = m_var.get_string(4).strip_edges()
var default_val: String = m_var.get_string(5).strip_edges()
# Try to infer type from default value if no explicit type
if var_type.is_empty() and not default_val.is_empty():
var_type = _infer_type(default_val)
# Track variable types for signal connection resolution
if not var_type.is_empty():
var_type_map[var_name] = var_type
variables.append({
&"name": var_name,
&"type": var_type,
&"exported": exported,
&"onready": onready,
&"default": default_val
})
# Functions
var m_func := re_func.search(stripped)
if m_func:
var func_name: String = m_func.get_string(1)
var return_type: String = m_func.get_string(3).strip_edges()
func_starts.append({&"line_idx": i, &"name": func_name})
functions.append({
&"name": func_name,
&"params": m_func.get_string(2).strip_edges(),
&"return_type": return_type,
&"line": i + 1,
&"body": "" # filled in second pass
})
# Signals
var m_sig := re_signal.search(stripped)
if m_sig:
signals_list.append({
&"name": m_sig.get_string(1),
&"params": m_sig.get_string(2).strip_edges() if m_sig.get_string(2) else ""
})
# Preload/load references
var m_preload := re_preload.search(stripped)
if m_preload:
preloads.append(m_preload.get_string(1))
# Signal connections (Godot 4 style)
# Pattern: obj.signal.connect(...) - e.g. wave_manager.wave_started.connect(...)
var m_conn_obj := re_connect_obj.search(stripped)
if m_conn_obj:
var obj_name: String = m_conn_obj.get_string(1)
var signal_name: String = m_conn_obj.get_string(2)
var target_type: String = var_type_map.get(obj_name, "")
connections.append({
&"object": obj_name,
&"signal": signal_name,
&"target": target_type,
&"line": i + 1
})
else:
# Pattern: signal.connect(...) - e.g. body_entered.connect(...)
var m_conn_direct := re_connect_direct.search(stripped)
if m_conn_direct:
connections.append({
&"signal": m_conn_direct.get_string(1),
&"target": extends_class, # Direct signal likely from parent class
&"line": i + 1
})
# Second pass: extract function bodies
for fi: int in range(func_starts.size()):
var start_idx: int = func_starts[fi][&"line_idx"]
var end_idx: int
if fi + 1 < func_starts.size():
end_idx = func_starts[fi + 1][&"line_idx"]
else:
end_idx = line_count
# Find actual end: look backwards from next func to skip blank lines
while end_idx > start_idx + 1 and lines[end_idx - 1].strip_edges().is_empty():
end_idx -= 1
# Also check for top-level declarations (var, signal, @export, class_name, etc.)
# that would end the function body
for check_idx in range(start_idx + 1, end_idx):
var check_line: String = lines[check_idx]
# If line is not indented and not empty and not a comment, it's not part of the function
if not check_line.is_empty() and not check_line.begins_with("\t") and not check_line.begins_with(" ") and not check_line.begins_with("#"):
end_idx = check_idx
break
var body_lines: PackedStringArray = PackedStringArray()
for li: int in range(start_idx, end_idx):
body_lines.append(lines[li])
var body: String = "\n".join(body_lines)
# Cap body size to avoid huge payloads
if body.length() > 3000:
body = body.substr(0, 3000) + "\n# ... (truncated)"
functions[fi][&"body"] = body
functions[fi][&"body_lines"] = end_idx - start_idx
# Determine folder
var folder: String = path.get_base_dir()
var filename: String = path.get_file()
return {
&"path": path,
&"filename": filename,
&"folder": folder,
&"class_name": class_name_str,
&"extends": extends_class,
&"description": description,
&"line_count": line_count,
&"variables": variables,
&"functions": functions,
&"signals": signals_list,
&"preloads": preloads,
&"connections": connections
}
func _infer_type(default_val: String) -> String:
"""Try to infer GDScript type from a default value."""
if default_val == "true" or default_val == "false":
return "bool"
if default_val.is_valid_int():
return "int"
if default_val.is_valid_float():
return "float"
if default_val.begins_with("\"") or default_val.begins_with("'"):
return "String"
if default_val.begins_with("Vector2"):
return "Vector2"
if default_val.begins_with("Vector3"):
return "Vector3"
if default_val.begins_with("Color"):
return "Color"
if default_val.begins_with("["):
return "Array"
if default_val.begins_with("{"):
return "Dictionary"
if default_val == "null":
return "Variant"
if default_val.ends_with(".new()"):
return default_val.replace(".new()", "")
return ""
func map_scenes(args: Dictionary) -> Dictionary:
"""Crawl the project and build a map of all scenes."""
var root_path: String = str(args.get(&"root", "res://"))
var include_addons: bool = bool(args.get(&"include_addons", false))
if not root_path.begins_with("res://"):
root_path = "res://" + root_path
# Collect all .tscn files
var scene_paths: Array = []
_collect_scenes(root_path, scene_paths, include_addons)
if scene_paths.is_empty():
return {&"ok": true, &"scene_map": {&"scenes": [], &"total_scenes": 0}}
# Parse each scene
var scenes: Array = []
for path: String in scene_paths:
var info: Dictionary = _parse_scene(path)
scenes.append(info)
# Build edges between scenes (instantiation, preloads)
var edges: Array = []
for scene: Dictionary in scenes:
var from_path: String = scene[&"path"]
for instance: String in scene.get(&"instances", []):
edges.append({&"from": from_path, &"to": instance, &"type": "instance"})
return {
&"ok": true,
&"scene_map": {
&"scenes": scenes,
&"edges": edges,
&"total_scenes": scenes.size()
}
}
func _collect_scenes(path: String, results: Array, include_addons: bool) -> void:
"""Recursively collect all .tscn files."""
var dir := DirAccess.open(path)
if dir == null:
return
dir.list_dir_begin()
var name := dir.get_next()
while name != "":
if name.begins_with("."):
name = dir.get_next()
continue
var full_path := path.path_join(name)
if dir.current_is_dir():
if name == "addons" and not include_addons:
name = dir.get_next()
continue
_collect_scenes(full_path, results, include_addons)
elif name.ends_with(".tscn"):
results.append(full_path)
name = dir.get_next()
dir.list_dir_end()
func _parse_scene(path: String) -> Dictionary:
"""Parse a scene file and extract its structure."""
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return {&"path": path, &"error": "Cannot open file"}
var content: String = file.get_as_text()
file.close()
var scene_name: String = path.get_file().replace(".tscn", "")
var root_type: String = ""
var nodes: Array = []
var instances: Array = []
var scripts: Array = []
# Parse .tscn format
var lines: PackedStringArray = content.split("\n")
var current_node: Dictionary = {}
var re_ext_resource := RegEx.new()
re_ext_resource.compile('\\[ext_resource.*path="([^"]+)".*type="([^"]+)"')
var re_node := RegEx.new()
re_node.compile('\\[node name="([^"]+)".*type="([^"]+)"')
var re_node_instance := RegEx.new()
re_node_instance.compile('\\[node name="([^"]+)".*instance=ExtResource\\("([^"]+)"\\)')
var re_script := RegEx.new()
re_script.compile('script = ExtResource\\("([^"]+)"\\)')
var ext_resources: Dictionary = {} # id -> {path, type}
for line: String in lines:
# External resources
var m_ext := re_ext_resource.search(line)
if m_ext:
var res_path: String = m_ext.get_string(1)
var res_type: String = m_ext.get_string(2)
# Extract ID from the line
var id_match := RegEx.create_from_string('id="([^"]+)"').search(line)
if id_match:
ext_resources[id_match.get_string(1)] = {&"path": res_path, &"type": res_type}
if res_type == "PackedScene":
instances.append(res_path)
elif res_type == "Script":
scripts.append(res_path)
continue
# Regular nodes
var m_node := re_node.search(line)
if m_node:
var node_name: String = m_node.get_string(1)
var node_type: String = m_node.get_string(2)
if root_type.is_empty():
root_type = node_type
nodes.append({&"name": node_name, &"type": node_type})
continue
# Instance nodes
var m_inst := re_node_instance.search(line)
if m_inst:
var node_name: String = m_inst.get_string(1)
nodes.append({&"name": node_name, &"type": "Instance"})
return {
&"path": path,
&"name": scene_name,
&"root_type": root_type,
&"nodes": nodes,
&"instances": instances,
&"scripts": scripts,
&"node_count": nodes.size()
}
# ============================================================================
# INTERNAL FILE MODIFICATION FUNCTIONS (not exposed as MCP tools)
# These are called directly by the visualizer for inline editing
# ============================================================================
func _internal_map_scenes(args: Dictionary) -> Dictionary:
"""Internal wrapper for map_scenes."""
return map_scenes(args)
func _internal_refresh_map(args: Dictionary) -> Dictionary:
"""Refresh the project map."""
return map_project(args)
func _internal_create_script_file(args: Dictionary) -> Dictionary:
"""Create a new script file."""
var script_path: String = args.get(&"path", "")
var extends_type: String = args.get(&"extends", "Node")
var class_name_str: String = args.get(&"class_name", "")
if script_path.is_empty():
return {&"ok": false, &"error": "No path provided"}
if not script_path.begins_with("res://"):
script_path = "res://" + script_path
if not script_path.ends_with(".gd"):
script_path += ".gd"
# Check if file already exists
if FileAccess.file_exists(script_path):
return {&"ok": false, &"error": "File already exists: " + script_path}
# Create directory if needed
var dir_path: String = script_path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
var err := DirAccess.make_dir_recursive_absolute(dir_path)
if err != OK:
return {&"ok": false, &"error": "Failed to create directory"}
# Build script content
var content := ""
if not class_name_str.is_empty():
content += "class_name " + class_name_str + "\n"
content += "extends " + extends_type + "\n"
content += "\n\n"
content += "func _ready() -> void:\n"
content += "\tpass\n"
# Write file
var file := FileAccess.open(script_path, FileAccess.WRITE)
if file == null:
return {&"ok": false, &"error": "Cannot create file: " + script_path}
file.store_string(content)
file.close()
return {&"ok": true, &"path": script_path}
func _internal_modify_variable(args: Dictionary) -> Dictionary:
"""Add, update, or delete a variable in a script file."""
var script_path: String = args.get(&"path", "")
var action: String = args.get(&"action", "") # "add", "update", "delete"
var old_name: String = args.get(&"old_name", "")
var new_name: String = args.get(&"name", "")
var var_type: String = args.get(&"type", "")
var default_val: String = args.get(&"default", "")
var exported: bool = args.get(&"exported", false)
var onready: bool = args.get(&"onready", false)
if script_path.is_empty():
return {&"ok": false, &"error": "No script path provided"}
var file := FileAccess.open(script_path, FileAccess.READ)
if file == null:
return {&"ok": false, &"error": "Cannot open file: " + script_path}
var content: String = file.get_as_text()
file.close()
var lines: Array = Array(content.split("\n"))
var modified := false
if action == "delete":
# Find and remove the variable declaration
var pattern := RegEx.new()
pattern.compile("^(@export(?:\\([^)]*\\))?\\s+)?(?:@onready\\s+)?var\\s+" + old_name + "\\s*(?::|=|$)")
for i: int in range(lines.size() - 1, -1, -1):
if pattern.search(lines[i].strip_edges()):
lines.remove_at(i)
modified = true
break
elif action == "update":
# Find and update the variable declaration
var pattern := RegEx.new()
pattern.compile("^(@export(?:\\([^)]*\\))?\\s+)?(@onready\\s+)?var\\s+" + old_name + "\\s*(?::\\s*\\w+)?(?:\\s*=\\s*.+)?$")
for i: int in range(lines.size()):
var m := pattern.search(lines[i].strip_edges())
if m:
# Use onready from args, not from matched pattern
var new_line := _build_var_line(new_name, var_type, default_val, exported, onready)
lines[i] = new_line
modified = true
break
elif action == "add":
# Find position to insert (after last variable, before first function)
var insert_pos := _find_var_insert_position(lines, exported)
var new_line := _build_var_line(new_name, var_type, default_val, exported, false)
lines.insert(insert_pos, new_line)
modified = true
if modified:
var new_content := "\n".join(PackedStringArray(lines))
var write_file := FileAccess.open(script_path, FileAccess.WRITE)
if write_file == null:
return {&"ok": false, &"error": "Cannot write to file: " + script_path}
write_file.store_string(new_content)
write_file.close()
return {&"ok": true, &"action": action, &"variable": new_name}
return {&"ok": false, &"error": "Variable not found: " + old_name}
func _internal_modify_signal(args: Dictionary) -> Dictionary:
"""Add, update, or delete a signal in a script file."""
var script_path: String = args.get(&"path", "")
var action: String = args.get(&"action", "")
var old_name: String = args.get(&"old_name", "")
var new_name: String = args.get(&"name", "")
var params: String = args.get(&"params", "")
if script_path.is_empty():
return {&"ok": false, &"error": "No script path provided"}
var file := FileAccess.open(script_path, FileAccess.READ)
if file == null:
return {&"ok": false, &"error": "Cannot open file: " + script_path}
var content: String = file.get_as_text()
file.close()
var lines: Array = Array(content.split("\n"))
var modified := false
if action == "delete":
var pattern := RegEx.new()
pattern.compile("^signal\\s+" + old_name + "(?:\\s*\\(|$)")
for i: int in range(lines.size() - 1, -1, -1):
if pattern.search(lines[i].strip_edges()):
lines.remove_at(i)
modified = true
break
elif action == "update":
var pattern := RegEx.new()
pattern.compile("^signal\\s+" + old_name + "(?:\\s*\\([^)]*\\))?$")
for i: int in range(lines.size()):
if pattern.search(lines[i].strip_edges()):
var new_line := "signal " + new_name
if not params.is_empty():
new_line += "(" + params + ")"
lines[i] = new_line
modified = true
break
elif action == "add":
var insert_pos := _find_signal_insert_position(lines)
var new_line := "signal " + new_name
if not params.is_empty():
new_line += "(" + params + ")"
lines.insert(insert_pos, new_line)
modified = true
if modified:
var new_content := "\n".join(PackedStringArray(lines))
var write_file := FileAccess.open(script_path, FileAccess.WRITE)
if write_file == null:
return {&"ok": false, &"error": "Cannot write to file: " + script_path}
write_file.store_string(new_content)
write_file.close()
return {&"ok": true, &"action": action, &"signal": new_name}
return {&"ok": false, &"error": "Signal not found: " + old_name}
func _internal_modify_function(args: Dictionary) -> Dictionary:
"""Update a function's body in a script file."""
var script_path: String = args.get(&"path", "")
var func_name: String = args.get(&"name", "")
var new_body: String = args.get(&"body", "")
if script_path.is_empty() or func_name.is_empty():
return {&"ok": false, &"error": "Missing path or function name"}
var file := FileAccess.open(script_path, FileAccess.READ)
if file == null:
return {&"ok": false, &"error": "Cannot open file: " + script_path}
var content: String = file.get_as_text()
file.close()
var lines: Array = Array(content.split("\n"))
# Find the function
var re_func := RegEx.new()
re_func.compile("^func\\s+" + func_name + "\\s*\\(")
var func_start := -1
var func_end := -1
for i: int in range(lines.size()):
if func_start == -1:
if re_func.search(lines[i].strip_edges()):
func_start = i
elif func_start != -1:
# Find end of function (next top-level declaration or end of file)
var stripped: String = lines[i].strip_edges()
if not stripped.is_empty() and not lines[i].begins_with("\t") and not lines[i].begins_with(" ") and not stripped.begins_with("#"):
func_end = i
break
if func_start == -1:
return {&"ok": false, &"error": "Function not found: " + func_name}
if func_end == -1:
func_end = lines.size()
# Remove trailing empty lines from function body
while func_end > func_start + 1 and lines[func_end - 1].strip_edges().is_empty():
func_end -= 1
# Replace function body
var new_lines := Array(new_body.split("\n"))
# Remove old function lines
for i: int in range(func_end - 1, func_start - 1, -1):
lines.remove_at(i)
# Insert new function lines
for i: int in range(new_lines.size()):
lines.insert(func_start + i, new_lines[i])
var new_content := "\n".join(PackedStringArray(lines))
var write_file := FileAccess.open(script_path, FileAccess.WRITE)
if write_file == null:
return {&"ok": false, &"error": "Cannot write to file: " + script_path}
write_file.store_string(new_content)
write_file.close()
return {&"ok": true, &"function": func_name}
func _internal_modify_function_delete(args: Dictionary) -> Dictionary:
"""Delete a function from a script file."""
var script_path: String = args.get(&"path", "")
var func_name: String = args.get(&"name", "")
if script_path.is_empty() or func_name.is_empty():
return {&"ok": false, &"error": "Missing path or function name"}
var file := FileAccess.open(script_path, FileAccess.READ)
if file == null:
return {&"ok": false, &"error": "Cannot open file: " + script_path}
var content: String = file.get_as_text()
file.close()
var lines: Array = Array(content.split("\n"))
# Find the function
var re_func := RegEx.new()
re_func.compile("^func\\s+" + func_name + "\\s*\\(")
var func_start := -1
var func_end := -1
for i: int in range(lines.size()):
if func_start == -1:
if re_func.search(lines[i].strip_edges()):
func_start = i
elif func_start != -1:
# Find end of function (next top-level declaration or end of file)
var stripped: String = lines[i].strip_edges()
if not stripped.is_empty() and not lines[i].begins_with("\t") and not lines[i].begins_with(" ") and not stripped.begins_with("#"):
func_end = i
break
if func_start == -1:
return {&"ok": false, &"error": "Function not found: " + func_name}
if func_end == -1:
func_end = lines.size()
# Remove trailing empty lines before the function
while func_end > func_start + 1 and lines[func_end - 1].strip_edges().is_empty():
func_end -= 1
# Remove the function lines
for i: int in range(func_end - 1, func_start - 1, -1):
lines.remove_at(i)
# Remove extra blank lines that might be left
# (but keep at least one blank line between declarations)
var new_content := "\n".join(PackedStringArray(lines))
var write_file := FileAccess.open(script_path, FileAccess.WRITE)
if write_file == null:
return {&"ok": false, &"error": "Cannot write to file: " + script_path}
write_file.store_string(new_content)
write_file.close()
return {&"ok": true, &"deleted": func_name}
func _internal_find_usages(args: Dictionary) -> Dictionary:
"""Find all usages of a variable, signal, or function across all scripts."""
var name: String = args.get(&"name", "")
var item_type: String = args.get(&"type", "") # "variable", "signal", "function"
var root_path: String = args.get(&"root", "res://")
if name.is_empty():
return {&"ok": false, &"error": "No name provided"}
# Collect all scripts
var script_paths: Array = []
_collect_scripts(root_path, script_paths, false)
var usages: Array = []
# Build regex pattern based on type
var pattern := RegEx.new()
if item_type == "signal":
# Match: signal_name.emit() or signal_name.connect() or .signal_name.
pattern.compile("\\b" + name + "\\b")
else:
# Match word boundary for variables and functions
pattern.compile("\\b" + name + "\\b")
for path: String in script_paths:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
continue
var content: String = file.get_as_text()
file.close()
var lines: PackedStringArray = content.split("\n")
for i: int in range(lines.size()):
var line: String = lines[i]
if pattern.search(line):
# Skip the declaration line itself
if item_type == "variable" and RegEx.create_from_string("^\\s*(@export)?\\s*var\\s+" + name + "\\b").search(line):
continue
if item_type == "signal" and RegEx.create_from_string("^\\s*signal\\s+" + name + "\\b").search(line):
continue
if item_type == "function" and RegEx.create_from_string("^\\s*func\\s+" + name + "\\s*\\(").search(line):
continue
usages.append({
&"file": path,
&"line": i + 1,
&"code": line.strip_edges()
})
return {&"ok": true, &"usages": usages, &"count": usages.size()}
# Helper functions for file modification
func _build_var_line(name: String, type: String, default: String, exported: bool, onready: bool) -> String:
var line := ""
if exported:
line += "@export "
if onready:
line += "@onready "
line += "var " + name
if not type.is_empty():
line += ": " + type
if not default.is_empty():
line += " = " + default
return line
func _find_var_insert_position(lines: Array, exported: bool) -> int:
"""Find the best position to insert a new variable."""
var last_var_line := -1
var first_func_line := -1
var after_class_decl := 0
var re_var := RegEx.new()
re_var.compile("^(@export)?\\s*(@onready)?\\s*var\\s+")
var re_func := RegEx.new()
re_func.compile("^func\\s+")
var re_class := RegEx.new()
re_class.compile("^(class_name|extends)\\s+")
for i: int in range(lines.size()):
var stripped: String = lines[i].strip_edges()
if re_class.search(stripped):
after_class_decl = i + 1
if re_var.search(stripped):
last_var_line = i
if re_func.search(stripped) and first_func_line == -1:
first_func_line = i
break
# Insert after last variable, or before first function, or after class declarations
if last_var_line != -1:
return last_var_line + 1
if first_func_line != -1:
return first_func_line
return max(after_class_decl, 2) # At least after extends/class_name
func _find_signal_insert_position(lines: Array) -> int:
"""Find the best position to insert a new signal."""
var last_signal_line := -1
var first_var_line := -1
var after_class_decl := 0
var re_signal := RegEx.new()
re_signal.compile("^signal\\s+")
var re_var := RegEx.new()
re_var.compile("^(@export)?\\s*var\\s+")
var re_class := RegEx.new()
re_class.compile("^(class_name|extends)\\s+")
for i: int in range(lines.size()):
var stripped: String = lines[i].strip_edges()
if re_class.search(stripped):
after_class_decl = i + 1
if re_signal.search(stripped):
last_signal_line = i
if re_var.search(stripped) and first_var_line == -1:
first_var_line = i
# Insert after last signal, or before first var, or after class declarations
if last_signal_line != -1:
return last_signal_line + 1
if first_var_line != -1:
return first_var_line
return max(after_class_decl, 2)
# ============================================================================
# SCENE VISUALIZER INTERNAL FUNCTIONS
# These are called by the visualizer for scene view functionality
# ============================================================================
func _internal_get_scene_hierarchy(args: Dictionary) -> Dictionary:
"""Get scene hierarchy for visualizer."""
if _scene_tools_ref and _scene_tools_ref.has_method("get_scene_hierarchy"):
return _scene_tools_ref.get_scene_hierarchy(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_get_scene_node_properties(args: Dictionary) -> Dictionary:
"""Get node properties for visualizer panel."""
if _scene_tools_ref and _scene_tools_ref.has_method("get_scene_node_properties"):
return _scene_tools_ref.get_scene_node_properties(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_set_scene_node_property(args: Dictionary) -> Dictionary:
"""Set node property from visualizer panel."""
if _scene_tools_ref and _scene_tools_ref.has_method("set_scene_node_property"):
return _scene_tools_ref.set_scene_node_property(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_add_scene_node(args: Dictionary) -> Dictionary:
"""Add a node to a scene from visualizer."""
if _scene_tools_ref and _scene_tools_ref.has_method("add_node"):
return _scene_tools_ref.add_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_remove_scene_node(args: Dictionary) -> Dictionary:
"""Remove a node from a scene from visualizer."""
if _scene_tools_ref and _scene_tools_ref.has_method("remove_node"):
return _scene_tools_ref.remove_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_rename_scene_node(args: Dictionary) -> Dictionary:
"""Rename a node in a scene from visualizer."""
if _scene_tools_ref and _scene_tools_ref.has_method("rename_node"):
return _scene_tools_ref.rename_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_move_scene_node(args: Dictionary) -> Dictionary:
"""Move/reorder a node in a scene from visualizer."""
if _scene_tools_ref and _scene_tools_ref.has_method("move_node"):
return _scene_tools_ref.move_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
# Simpler aliases for context menu actions
func _internal_add_node(args: Dictionary) -> Dictionary:
"""Add a node to a scene."""
if _scene_tools_ref and _scene_tools_ref.has_method("add_node"):
return _scene_tools_ref.add_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_remove_node(args: Dictionary) -> Dictionary:
"""Remove a node from a scene."""
if _scene_tools_ref and _scene_tools_ref.has_method("remove_node"):
return _scene_tools_ref.remove_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_rename_node(args: Dictionary) -> Dictionary:
"""Rename a node in a scene."""
if _scene_tools_ref and _scene_tools_ref.has_method("rename_node"):
return _scene_tools_ref.rename_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_move_node(args: Dictionary) -> Dictionary:
"""Move/reorder a node in a scene."""
if _scene_tools_ref and _scene_tools_ref.has_method("move_node"):
return _scene_tools_ref.move_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_duplicate_node(args: Dictionary) -> Dictionary:
"""Duplicate a node in a scene."""
if _scene_tools_ref and _scene_tools_ref.has_method("duplicate_node"):
return _scene_tools_ref.duplicate_node(args)
return {&"ok": false, &"error": "Scene tools not available"}
func _internal_reorder_node(args: Dictionary) -> Dictionary:
"""Reorder a node within its parent (change sibling index)."""
if _scene_tools_ref and _scene_tools_ref.has_method("reorder_node"):
return _scene_tools_ref.reorder_node(args)
return {&"ok": false, &"error": "Scene tools not available"}