gdharness 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +125 -0
  3. package/build/cli.js +30353 -0
  4. package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
  5. package/build/godot/addons/auto_reload/plugin.cfg +6 -0
  6. package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
  7. package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
  8. package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
  11. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
  20. package/build/godot/operations/audio_buses.gd +125 -0
  21. package/build/godot/operations/class_cache.gd +180 -0
  22. package/build/godot/operations/classdb_queries.gd +252 -0
  23. package/build/godot/operations/dependencies.gd +293 -0
  24. package/build/godot/operations/file_walk.gd +55 -0
  25. package/build/godot/operations/gdscript_analysis.gd +288 -0
  26. package/build/godot/operations/gdscript_authoring.gd +419 -0
  27. package/build/godot/operations/godot_operations.gd +186 -0
  28. package/build/godot/operations/import_pipeline.gd +390 -0
  29. package/build/godot/operations/input_actions.gd +237 -0
  30. package/build/godot/operations/logger.gd +33 -0
  31. package/build/godot/operations/plugins.gd +159 -0
  32. package/build/godot/operations/project_config.gd +153 -0
  33. package/build/godot/operations/project_diagnostics.gd +159 -0
  34. package/build/godot/operations/resource_files.gd +132 -0
  35. package/build/godot/operations/serialisation.gd +154 -0
  36. package/build/index.js +29252 -0
  37. package/package.json +57 -0
@@ -0,0 +1,89 @@
1
+ @tool
2
+ extends EditorPlugin
3
+
4
+ ## Reloads the open scene and the scripts it carries when they change on disk, without the
5
+ ## editor's confirmation popup. Every edit made through gdharness lands on disk, and an editor
6
+ ## that keeps showing the old file is how a change reads as one that never happened. The
7
+ ## files are polled once a second; the editor's own watcher only fires on window focus.
8
+
9
+ const CHECK_INTERVAL_SECONDS: float = 1.0
10
+
11
+ var _timer: Timer
12
+ ## The last modification time seen, by path.
13
+ var _watched_files: Dictionary = {}
14
+
15
+
16
+ func _enter_tree() -> void:
17
+ _timer = Timer.new()
18
+ _timer.wait_time = CHECK_INTERVAL_SECONDS
19
+ _timer.timeout.connect(_check_for_changes)
20
+ add_child(_timer)
21
+ _timer.start()
22
+ _update_watched_files()
23
+
24
+
25
+ func _exit_tree() -> void:
26
+ if _timer:
27
+ _timer.stop()
28
+ _timer.queue_free()
29
+
30
+
31
+ func _update_watched_files() -> void:
32
+ var edited_scene: Node = EditorInterface.get_edited_scene_root()
33
+ if edited_scene and edited_scene.scene_file_path:
34
+ var path: String = edited_scene.scene_file_path
35
+ if not _watched_files.has(path):
36
+ _watched_files[path] = _get_modified_time(path)
37
+ _watch_node_scripts(edited_scene)
38
+
39
+
40
+ func _watch_node_scripts(node: Node) -> void:
41
+ var script: Variant = node.get_script()
42
+ if script is Script:
43
+ var attached: Script = script
44
+ var path: String = attached.resource_path
45
+ if not path.is_empty() and not _watched_files.has(path):
46
+ _watched_files[path] = _get_modified_time(path)
47
+ for child: Node in node.get_children():
48
+ _watch_node_scripts(child)
49
+
50
+
51
+ func _get_modified_time(path: String) -> int:
52
+ var global_path: String = ProjectSettings.globalize_path(path)
53
+ if FileAccess.file_exists(global_path):
54
+ return FileAccess.get_modified_time(global_path)
55
+ return 0
56
+
57
+
58
+ func _check_for_changes() -> void:
59
+ _update_watched_files()
60
+
61
+ var scenes_to_reload: Array[String] = []
62
+ var scripts_to_reload: Array[String] = []
63
+ for path: String in _watched_files.keys():
64
+ var current_time: int = _get_modified_time(path)
65
+ var last_time: int = _watched_files[path]
66
+ if current_time > last_time:
67
+ if path.ends_with(".gd"):
68
+ scripts_to_reload.append(path)
69
+ else:
70
+ scenes_to_reload.append(path)
71
+ _watched_files[path] = current_time
72
+
73
+ # Scripts first, so a scene reloaded after them instantiates the new code.
74
+ for path: String in scripts_to_reload:
75
+ _reload_script(path)
76
+ for path: String in scenes_to_reload:
77
+ _reload_scene(path)
78
+
79
+
80
+ func _reload_script(path: String) -> void:
81
+ print("[gdharness] Script changed on disk, reloading: ", path)
82
+ ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
83
+
84
+
85
+ func _reload_scene(path: String) -> void:
86
+ var edited_scene: Node = EditorInterface.get_edited_scene_root()
87
+ if edited_scene and edited_scene.scene_file_path == path:
88
+ print("[gdharness] Scene changed on disk, reloading: ", path)
89
+ EditorInterface.reload_scene_from_path(path)
@@ -0,0 +1,6 @@
1
+ [plugin]
2
+ name="gdharness auto reload"
3
+ description="Reloads scenes and scripts in the editor when a file changes on disk, which is how every edit made through gdharness arrives."
4
+ author="gdharness"
5
+ version="0.1.0"
6
+ script="auto_reload.gd"
@@ -0,0 +1,197 @@
1
+ @tool
2
+ extends Node
3
+
4
+ ## The editor's end of the bridge: one WebSocket to the gdharness server, reconnected on its
5
+ ## own when it drops, carrying tool requests in and their results out.
6
+
7
+ signal connected
8
+ signal disconnected
9
+ signal tool_requested(request_id: String, tool_name: String, args: Dictionary)
10
+
11
+ const DEFAULT_URL: String = "ws://127.0.0.1:6505/godot"
12
+ ## Written beside the addon by the install, so it names the version this copy came from.
13
+ const VERSION_MARKER: String = "res://addons/gdharness_editor/.gdharness-version"
14
+ const RECONNECT_DELAY: float = 3.0
15
+ const MAX_RECONNECT_DELAY: float = 30.0
16
+
17
+ var socket: WebSocketPeer = WebSocketPeer.new()
18
+ var server_url: String = DEFAULT_URL
19
+ var _is_connected: bool = false
20
+ var _reconnect_timer: Timer
21
+ var _current_reconnect_delay: float = RECONNECT_DELAY
22
+ var _should_reconnect: bool = true
23
+ var _project_path: String
24
+ var _initialized: bool = false
25
+
26
+
27
+ func _ready() -> void:
28
+ _project_path = ProjectSettings.globalize_path("res://")
29
+
30
+ _reconnect_timer = Timer.new()
31
+ _reconnect_timer.one_shot = true
32
+ _reconnect_timer.timeout.connect(_on_reconnect_timer)
33
+ add_child(_reconnect_timer)
34
+
35
+ set_process(true)
36
+ _initialized = true
37
+
38
+
39
+ func _process(_delta: float) -> void:
40
+ if not _initialized:
41
+ return
42
+
43
+ if socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
44
+ if _is_connected:
45
+ _handle_disconnect()
46
+ return
47
+
48
+ socket.poll()
49
+
50
+ match socket.get_ready_state():
51
+ WebSocketPeer.STATE_OPEN:
52
+ if not _is_connected:
53
+ _handle_connect()
54
+
55
+ while socket.get_available_packet_count() > 0:
56
+ var packet: PackedByteArray = socket.get_packet()
57
+ _handle_message(packet.get_string_from_utf8())
58
+
59
+ WebSocketPeer.STATE_CLOSING:
60
+ pass
61
+
62
+ WebSocketPeer.STATE_CLOSED:
63
+ if _is_connected:
64
+ _handle_disconnect()
65
+
66
+
67
+ func connect_to_server(url: String = "") -> void:
68
+ server_url = _resolve_server_url(url)
69
+ _should_reconnect = true
70
+ _current_reconnect_delay = RECONNECT_DELAY
71
+ _attempt_connection()
72
+
73
+
74
+ func _resolve_server_url(explicit_url: String) -> String:
75
+ if explicit_url != "":
76
+ return explicit_url
77
+
78
+ # The same variable the server reads, so the two agree on the port by construction.
79
+ var raw: String = OS.get_environment("GDHARNESS_BRIDGE_PORT")
80
+ if raw != "":
81
+ if raw.is_valid_int() and int(raw) >= 1 and int(raw) <= 65535:
82
+ return "ws://127.0.0.1:%d/godot" % int(raw)
83
+ push_error("GDHARNESS_BRIDGE_PORT is %s, not a port; using %s" % [raw, DEFAULT_URL])
84
+
85
+ return DEFAULT_URL
86
+
87
+
88
+ func disconnect_from_server() -> void:
89
+ _should_reconnect = false
90
+ if _reconnect_timer:
91
+ _reconnect_timer.stop()
92
+ if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
93
+ socket.close()
94
+ _is_connected = false
95
+
96
+
97
+ func _attempt_connection() -> void:
98
+ if socket.get_ready_state() != WebSocketPeer.STATE_CLOSED:
99
+ socket.close()
100
+
101
+ var err: Error = socket.connect_to_url(server_url)
102
+ if err != OK:
103
+ push_error("[gdharness] Failed to connect to %s: %s" % [server_url, error_string(err)])
104
+ _schedule_reconnect()
105
+
106
+
107
+ func _handle_connect() -> void:
108
+ _is_connected = true
109
+ _current_reconnect_delay = RECONNECT_DELAY
110
+
111
+ # The version reported is the one this editor loaded at startup, not the one on disk: an
112
+ # upgrade replaces the files under a running editor, which goes on serving the old code until
113
+ # somebody restarts it, and nothing else can tell the two apart.
114
+ # The process id with it, because a restarted editor is a different process from the one
115
+ # whoever started it is holding, and nothing else says which one is now on the other end.
116
+ _send_message(
117
+ {
118
+ "type": "godot_ready",
119
+ "project_path": _project_path,
120
+ "addon_version": _loaded_version(),
121
+ "editor_pid": OS.get_process_id()
122
+ }
123
+ )
124
+
125
+ connected.emit()
126
+
127
+
128
+ ## The version marker beside this addon, or "" when the copy was not installed by gdharness.
129
+ func _loaded_version() -> String:
130
+ if not FileAccess.file_exists(VERSION_MARKER):
131
+ return ""
132
+ var file: FileAccess = FileAccess.open(VERSION_MARKER, FileAccess.READ)
133
+ if file == null:
134
+ return ""
135
+ var text: String = file.get_as_text().strip_edges()
136
+ file.close()
137
+ return text
138
+
139
+
140
+ func _handle_disconnect() -> void:
141
+ _is_connected = false
142
+ disconnected.emit()
143
+
144
+ if _should_reconnect:
145
+ _schedule_reconnect()
146
+
147
+
148
+ func _schedule_reconnect() -> void:
149
+ if _reconnect_timer == null:
150
+ return
151
+ _reconnect_timer.start(_current_reconnect_delay)
152
+ _current_reconnect_delay = min(_current_reconnect_delay * 2.0, MAX_RECONNECT_DELAY)
153
+
154
+
155
+ func _on_reconnect_timer() -> void:
156
+ _attempt_connection()
157
+
158
+
159
+ func _handle_message(json_string: String) -> void:
160
+ var parsed: Variant = JSON.parse_string(json_string)
161
+ if not parsed is Dictionary:
162
+ push_error("[gdharness] The server sent something that is not a message: %s" % json_string)
163
+ return
164
+ var message: Dictionary = parsed
165
+
166
+ match message.get("type", ""):
167
+ "ping":
168
+ _send_message({"type": "pong"})
169
+
170
+ "tool_invoke":
171
+ var request_id: String = message.get("id", "")
172
+ var tool_name: String = message.get("tool", "")
173
+ var args: Dictionary = message.get("args", {})
174
+ tool_requested.emit(request_id, tool_name, args)
175
+
176
+ _:
177
+ pass
178
+
179
+
180
+ func send_tool_result(request_id: String, success: bool, result: Variant = null, error: String = "") -> void:
181
+ var response: Dictionary = {"type": "tool_result", "id": request_id, "success": success}
182
+
183
+ if success:
184
+ response["result"] = result
185
+ else:
186
+ response["error"] = error
187
+
188
+ _send_message(response)
189
+
190
+
191
+ func _send_message(message: Dictionary) -> void:
192
+ if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
193
+ socket.send_text(JSON.stringify(message))
194
+
195
+
196
+ func is_connected_to_server() -> bool:
197
+ return _is_connected
@@ -0,0 +1,6 @@
1
+ [plugin]
2
+ name="gdharness editor"
3
+ description="Connects the editor to the gdharness server, which drives scenes, resources and the filesystem through it."
4
+ author="gdharness"
5
+ version="0.1.0"
6
+ script="plugin.gd"
@@ -0,0 +1,88 @@
1
+ @tool
2
+ extends EditorPlugin
3
+
4
+ ## The editor half of gdharness: a client that holds a socket to the server and an executor
5
+ ## that does what the server asks, with a label in the toolbar saying whether the two are
6
+ ## connected.
7
+
8
+ const BridgeClient = preload("bridge_client.gd")
9
+ const ToolExecutor = preload("tool_executor.gd")
10
+
11
+ var _client: BridgeClient
12
+ var _tool_executor: ToolExecutor
13
+ var _status_label: Label
14
+
15
+
16
+ func _enter_tree() -> void:
17
+ _client = BridgeClient.new()
18
+ _client.name = "GdharnessBridgeClient"
19
+ add_child(_client)
20
+
21
+ _tool_executor = ToolExecutor.new()
22
+ _tool_executor.name = "GdharnessToolExecutor"
23
+ add_child(_tool_executor)
24
+ _tool_executor.set_editor_plugin(self)
25
+
26
+ _client.connected.connect(_on_connected)
27
+ _client.disconnected.connect(_on_disconnected)
28
+ _client.tool_requested.connect(_on_tool_requested)
29
+
30
+ _setup_status_indicator()
31
+ _client.connect_to_server()
32
+
33
+
34
+ func _exit_tree() -> void:
35
+ if _client:
36
+ if _client.connected.is_connected(_on_connected):
37
+ _client.connected.disconnect(_on_connected)
38
+ if _client.disconnected.is_connected(_on_disconnected):
39
+ _client.disconnected.disconnect(_on_disconnected)
40
+ if _client.tool_requested.is_connected(_on_tool_requested):
41
+ _client.tool_requested.disconnect(_on_tool_requested)
42
+ _client.disconnect_from_server()
43
+ _client.queue_free()
44
+ _client = null
45
+
46
+ if _tool_executor:
47
+ _tool_executor.queue_free()
48
+ _tool_executor = null
49
+
50
+ if _status_label:
51
+ remove_control_from_container(CONTAINER_TOOLBAR, _status_label)
52
+ _status_label.queue_free()
53
+ _status_label = null
54
+
55
+
56
+ func _setup_status_indicator() -> void:
57
+ _status_label = Label.new()
58
+ _status_label.text = "gdharness: connecting"
59
+ _status_label.add_theme_color_override("font_color", Color.YELLOW)
60
+ _status_label.add_theme_font_size_override("font_size", 12)
61
+ add_control_to_container(CONTAINER_TOOLBAR, _status_label)
62
+
63
+
64
+ func _on_connected() -> void:
65
+ if _status_label:
66
+ _status_label.text = "gdharness: connected"
67
+ _status_label.add_theme_color_override("font_color", Color.GREEN)
68
+
69
+
70
+ func _on_disconnected() -> void:
71
+ if _status_label:
72
+ _status_label.text = "gdharness: disconnected"
73
+ _status_label.add_theme_color_override("font_color", Color.RED)
74
+
75
+
76
+ func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary) -> void:
77
+ if _tool_executor == null or _client == null:
78
+ return
79
+
80
+ var result: Dictionary = _tool_executor.execute_tool(tool_name, args)
81
+ var success: bool = result.get("ok", false)
82
+
83
+ if success:
84
+ var payload: Dictionary = result.duplicate(true)
85
+ payload.erase("ok")
86
+ _client.send_tool_result(request_id, true, payload, "")
87
+ else:
88
+ _client.send_tool_result(request_id, false, null, str(result.get("error", "Unknown error")))
@@ -0,0 +1,104 @@
1
+ @tool
2
+ extends Node
3
+
4
+ ## Routes each command the server sends to the tool module that answers it. The modules are
5
+ ## preloaded rather than looked up on disk at runtime, so a missing one fails to parse here
6
+ ## instead of leaving a command silently unanswered.
7
+
8
+ const SceneTools = preload("tools/scene_tools.gd")
9
+ const ResourceTools = preload("tools/resource_tools.gd")
10
+ const AnimationTools = preload("tools/animation_tools.gd")
11
+ const PlayTools = preload("tools/play_tools.gd")
12
+
13
+ var _editor_plugin: EditorPlugin = null
14
+
15
+ var _scene_tools: SceneTools = null
16
+ var _resource_tools: ResourceTools = null
17
+ var _animation_tools: AnimationTools = null
18
+ var _play_tools: PlayTools = null
19
+
20
+ var _tool_map: Dictionary = {}
21
+ var _initialized: bool = false
22
+
23
+
24
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
25
+ _editor_plugin = plugin
26
+ _init_tools()
27
+ _scene_tools.set_editor_plugin(plugin)
28
+ _resource_tools.set_editor_plugin(plugin)
29
+ _animation_tools.set_editor_plugin(plugin)
30
+ _play_tools.set_editor_plugin(plugin)
31
+
32
+
33
+ func _init_tools() -> void:
34
+ if _initialized:
35
+ return
36
+ _initialized = true
37
+
38
+ _scene_tools = SceneTools.new()
39
+ _scene_tools.name = "SceneTools"
40
+ add_child(_scene_tools)
41
+
42
+ _resource_tools = ResourceTools.new()
43
+ _resource_tools.name = "ResourceTools"
44
+ add_child(_resource_tools)
45
+
46
+ _animation_tools = AnimationTools.new()
47
+ _animation_tools.name = "AnimationTools"
48
+ add_child(_animation_tools)
49
+
50
+ _play_tools = PlayTools.new()
51
+ _play_tools.name = "PlayTools"
52
+ add_child(_play_tools)
53
+
54
+ _tool_map = {
55
+ # Scene tools
56
+ "create_scene": [_scene_tools, "create_scene"],
57
+ "list_scene_nodes": [_scene_tools, "list_scene_nodes"],
58
+ "add_node": [_scene_tools, "add_node"],
59
+ "delete_node": [_scene_tools, "delete_node"],
60
+ "duplicate_node": [_scene_tools, "duplicate_node"],
61
+ "reparent_node": [_scene_tools, "reparent_node"],
62
+ "set_node_properties": [_scene_tools, "set_node_properties"],
63
+ "get_node_properties": [_scene_tools, "get_node_properties"],
64
+ "save_scene": [_scene_tools, "save_scene"],
65
+ "connect_signal": [_scene_tools, "connect_signal"],
66
+ "disconnect_signal": [_scene_tools, "disconnect_signal"],
67
+ "list_connections": [_scene_tools, "list_connections"],
68
+ "rescan_filesystem": [_scene_tools, "rescan_filesystem"],
69
+ # Resource tools
70
+ "create_resource": [_resource_tools, "create_resource"],
71
+ "modify_resource": [_resource_tools, "modify_resource"],
72
+ "create_shader": [_resource_tools, "create_shader"],
73
+ "create_tileset": [_resource_tools, "create_tileset"],
74
+ "set_tilemap_cells": [_resource_tools, "set_tilemap_cells"],
75
+ "set_theme_color": [_resource_tools, "set_theme_color"],
76
+ "set_theme_font_size": [_resource_tools, "set_theme_font_size"],
77
+ # Animation tools
78
+ "play_scene": [_play_tools, "play_scene"],
79
+ "restart_editor": [_play_tools, "restart_editor"],
80
+ "stop_playing": [_play_tools, "stop_playing"],
81
+ "playing_status": [_play_tools, "playing_status"],
82
+ "create_animation": [_animation_tools, "create_animation"],
83
+ "add_animation_track": [_animation_tools, "add_animation_track"],
84
+ "add_animation_state": [_animation_tools, "add_animation_state"],
85
+ "connect_animation_states": [_animation_tools, "connect_animation_states"],
86
+ }
87
+
88
+
89
+ func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
90
+ if not _tool_map.has(tool_name):
91
+ return {"ok": false, "error": "Unknown tool: " + tool_name}
92
+
93
+ var handler: Array = _tool_map[tool_name]
94
+ var node: Node = handler[0]
95
+ var method: String = handler[1]
96
+
97
+ if not node.has_method(method):
98
+ return {"ok": false, "error": "Tool method not found: %s.%s" % [node.name, method]}
99
+
100
+ var result: Variant = node.call(method, args)
101
+ if result is Dictionary:
102
+ return result
103
+
104
+ return {"ok": false, "error": "Invalid tool result from: " + tool_name}