gdharness 1.3.6 → 1.3.8
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.
- package/build/cli.js +1602 -1374
- package/build/godot/addons/gdharness_editor/reading.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +12 -10
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +11 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +45 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +47 -23
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -2
- package/build/godot/operations/dependencies.gd +61 -10
- package/build/godot/operations/gdscript_analysis.gd +62 -6
- package/build/godot/operations/gdscript_authoring.gd +4 -1
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/import_pipeline.gd +45 -1
- package/build/godot/operations/project_diagnostics.gd +6 -5
- package/build/godot/operations/reading.gd +1 -1
- package/build/godot/tests/listed.gd +26 -4
- package/build/keeper.js +26 -3
- package/package.json +1 -1
|
@@ -152,7 +152,7 @@ static func not_of_kind(
|
|
|
152
152
|
if not found.is_empty():
|
|
153
153
|
told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
|
|
154
154
|
if holders.is_empty():
|
|
155
|
-
return told + " The scene holds no %s." % kind
|
|
155
|
+
return told + " The scene holds no %s; scene_node add with nodeType %s puts one in it." % [kind, kind]
|
|
156
156
|
var quoted: PackedStringArray = []
|
|
157
157
|
for holder: String in holders.slice(0, NAMED_CHILDREN):
|
|
158
158
|
var _quoted: bool = quoted.append('"%s"' % holder)
|
|
@@ -170,17 +170,18 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
170
170
|
)
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
var
|
|
174
|
-
if not
|
|
175
|
-
return
|
|
173
|
+
var saving: Dictionary = _save_everything()
|
|
174
|
+
if not saving.has("saved"):
|
|
175
|
+
return saving
|
|
176
176
|
|
|
177
177
|
# Deferred so this answer is on its way out before the editor goes.
|
|
178
178
|
EditorInterface.restart_editor.call_deferred(true)
|
|
179
179
|
|
|
180
|
-
return {"ok": true, "restarting": true, "saved":
|
|
180
|
+
return {"ok": true, "restarting": true, "saved": saving["saved"]}
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
## Saves every open scene, answering a refusal naming what is still unsaved, or
|
|
183
|
+
## Saves every open scene, answering a refusal naming what is still unsaved, or the scenes that had
|
|
184
|
+
## changes and were saved, under `saved`.
|
|
184
185
|
##
|
|
185
186
|
## Read back rather than assumed: save_all_scenes answers nothing and passes over a scene that has
|
|
186
187
|
## never been saved, which has no path to go to, so an untitled scene was lost with the editor while
|
|
@@ -188,10 +189,11 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
188
189
|
static func _save_everything() -> Dictionary:
|
|
189
190
|
if EditorInterface.is_playing_scene():
|
|
190
191
|
EditorInterface.stop_playing_scene()
|
|
192
|
+
var changed: PackedStringArray = EditorInterface.get_unsaved_scenes()
|
|
191
193
|
EditorInterface.save_all_scenes()
|
|
192
194
|
var left: PackedStringArray = EditorInterface.get_unsaved_scenes()
|
|
193
195
|
if left.is_empty():
|
|
194
|
-
return {}
|
|
196
|
+
return {"ok": true, "saved": changed}
|
|
195
197
|
var named: Array[String] = []
|
|
196
198
|
for scene: String in left:
|
|
197
199
|
named.append(scene if not scene.is_empty() else "a scene that has never been saved")
|
|
@@ -221,14 +223,14 @@ static func _save_everything() -> Dictionary:
|
|
|
221
223
|
## Saved first, exactly as the restart above saves. The editor is going either way, and unsaved
|
|
222
224
|
## scenes are not this tool's to lose.
|
|
223
225
|
func quit_editor(_args: Dictionary) -> Dictionary:
|
|
224
|
-
var
|
|
225
|
-
if not
|
|
226
|
-
return
|
|
226
|
+
var saving: Dictionary = _save_everything()
|
|
227
|
+
if not saving.has("saved"):
|
|
228
|
+
return saving
|
|
227
229
|
|
|
228
230
|
# Deferred so this answer is on its way out before the editor goes.
|
|
229
231
|
_leave.call_deferred()
|
|
230
232
|
|
|
231
|
-
return {"ok": true, "quitting": true, "saved":
|
|
233
|
+
return {"ok": true, "quitting": true, "saved": saving["saved"]}
|
|
232
234
|
|
|
233
235
|
|
|
234
236
|
## What the editor said as it started each of its servers, and the debug adapter started again when
|
|
@@ -285,6 +285,17 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
285
285
|
var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
286
286
|
if resource == null:
|
|
287
287
|
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
288
|
+
# A scene's own properties are its packed bundle, so a property named for a scene is one of its
|
|
289
|
+
# nodes', and "PackedScene has no property" left the caller looking for the tool that sets it.
|
|
290
|
+
if resource is PackedScene:
|
|
291
|
+
return {
|
|
292
|
+
"ok": false,
|
|
293
|
+
"error":
|
|
294
|
+
(
|
|
295
|
+
"%s is a scene: scene_node set changes one node's properties, the root's at nodePath \".\"."
|
|
296
|
+
% res_path
|
|
297
|
+
)
|
|
298
|
+
}
|
|
288
299
|
|
|
289
300
|
var properties: Dictionary = _parse_properties_dict(args.get("properties", ""))
|
|
290
301
|
var refused: String = _properties.write_all(resource, properties)
|
|
@@ -152,7 +152,7 @@ static func not_of_kind(
|
|
|
152
152
|
if not found.is_empty():
|
|
153
153
|
told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
|
|
154
154
|
if holders.is_empty():
|
|
155
|
-
return told + " The scene holds no %s." % kind
|
|
155
|
+
return told + " The scene holds no %s; scene_node add with nodeType %s puts one in it." % [kind, kind]
|
|
156
156
|
var quoted: PackedStringArray = []
|
|
157
157
|
for holder: String in holders.slice(0, NAMED_CHILDREN):
|
|
158
158
|
var _quoted: bool = quoted.append('"%s"' % holder)
|
|
@@ -67,6 +67,13 @@ const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
|
67
67
|
## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
|
|
68
68
|
const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
|
|
69
69
|
|
|
70
|
+
## The `res://` folders of scenes that are not the game, such as a bench: a run started on one is
|
|
71
|
+
## not served. Kept in step with `QUIET_SCENES_SETTING` in src/setup.ts.
|
|
72
|
+
const QUIET_SCENES_SETTING: String = "gdharness/runtime/quiet_scenes_under"
|
|
73
|
+
|
|
74
|
+
## What the engine takes a bare argument for as the scene to run, by its extension.
|
|
75
|
+
const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres"]
|
|
76
|
+
|
|
70
77
|
## The variable the editor addon puts into its own environment, which every game the editor plays
|
|
71
78
|
## inherits: it says which editor played this game, so a server can tell the editor's game from
|
|
72
79
|
## another of the same project that some other server started. Kept in step with
|
|
@@ -123,6 +130,9 @@ var _commands: Dictionary = {}
|
|
|
123
130
|
var _loaded_version: String = _read_marker(VERSION_MARKER)
|
|
124
131
|
var _loaded_digest: String = _read_marker(DIGEST_MARKER)
|
|
125
132
|
var _profile_received: bool = false
|
|
133
|
+
## Whether whatever started this game asked for a profile from its start, read before the variable
|
|
134
|
+
## is taken out of the environment.
|
|
135
|
+
var _profiled_from_start: bool = false
|
|
126
136
|
var _close_asked: bool = false
|
|
127
137
|
var _close_frames: int = 0
|
|
128
138
|
var _profiler_calibrated: bool = false
|
|
@@ -200,7 +210,9 @@ static func _window_dimension(axis: String, fallback: int) -> int:
|
|
|
200
210
|
func _wait_for_the_profiler() -> void:
|
|
201
211
|
if OS.get_environment(PROFILE_FROM_START).is_empty():
|
|
202
212
|
return
|
|
203
|
-
|
|
213
|
+
_profiled_from_start = true
|
|
214
|
+
# Read once: a process this game starts inherits its environment and must not wait as well, nor
|
|
215
|
+
# be served as a profiled run where a quiet scene would not be, as a bench's workers are.
|
|
204
216
|
OS.unset_environment(PROFILE_FROM_START)
|
|
205
217
|
if not EngineDebugger.is_active():
|
|
206
218
|
return
|
|
@@ -446,6 +458,15 @@ func _start_server() -> void:
|
|
|
446
458
|
_enabled = false
|
|
447
459
|
return
|
|
448
460
|
|
|
461
|
+
# Nor a scene the project says is not the game. A bench is handed to the engine as a scene, the
|
|
462
|
+
# way a screen is, so it announced as the game for as long as it ran, and so did every worker
|
|
463
|
+
# engine it started, all under the project's own path. One started to be profiled from its start
|
|
464
|
+
# is served all the same, since only this addon hands the totals back as it quits.
|
|
465
|
+
var prefixes: Variant = ProjectSettings.get_setting(QUIET_SCENES_SETTING, PackedStringArray())
|
|
466
|
+
if not _profiled_from_start and not quiet_scene_in(OS.get_cmdline_args(), prefixes).is_empty():
|
|
467
|
+
_enabled = false
|
|
468
|
+
return
|
|
469
|
+
|
|
449
470
|
_server = TCPServer.new()
|
|
450
471
|
# listen() defaults bind_address to "*", which exposes the game to the whole network.
|
|
451
472
|
var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
|
|
@@ -474,6 +495,29 @@ func _start_server() -> void:
|
|
|
474
495
|
print(listening)
|
|
475
496
|
|
|
476
497
|
|
|
498
|
+
## The scene [param given] names for the engine to run, as a `res://` path, when it lies under one of
|
|
499
|
+
## [param prefixes]; otherwise empty. The engine takes any bare argument ending in a scene's
|
|
500
|
+
## extension for the scene, relative to the project or absolute, so each is read as it would.
|
|
501
|
+
static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> String:
|
|
502
|
+
# A PackedStringArray as project.godot writes one, or an Array of strings as a hand might.
|
|
503
|
+
var folders: PackedStringArray = []
|
|
504
|
+
if prefixes is PackedStringArray:
|
|
505
|
+
folders = prefixes
|
|
506
|
+
elif prefixes is Array:
|
|
507
|
+
var listed: Array = prefixes
|
|
508
|
+
for prefix: Variant in listed:
|
|
509
|
+
if prefix is String:
|
|
510
|
+
var _kept: bool = folders.append(str(prefix))
|
|
511
|
+
for argument: String in given:
|
|
512
|
+
if argument.begins_with("-") or not SCENE_EXTENSIONS.has(argument.get_extension().to_lower()):
|
|
513
|
+
continue
|
|
514
|
+
var scene: String = ProjectSettings.localize_path(argument)
|
|
515
|
+
for folder: String in folders:
|
|
516
|
+
if not folder.is_empty() and scene.begins_with(folder):
|
|
517
|
+
return scene
|
|
518
|
+
return ""
|
|
519
|
+
|
|
520
|
+
|
|
477
521
|
## Whether the engine was told to run a script rather than the game.
|
|
478
522
|
func _script_run() -> bool:
|
|
479
523
|
var given: PackedStringArray = OS.get_cmdline_args()
|
|
@@ -10,6 +10,9 @@ const GdhScreen = preload("runtime_screen.gd")
|
|
|
10
10
|
## How far zoom enlarges at most. Sixteen makes a 3-pixel ring 48 pixels across, past any need to
|
|
11
11
|
## see it, and a 200-pixel region 3200, past the size anybody reads a picture at.
|
|
12
12
|
const ZOOM_MOST: int = 16
|
|
13
|
+
## Measured on a downstream game's 1280x720 frame: its interface text and pixel art read as they do
|
|
14
|
+
## in the PNG, at a fifth of the PNG's size.
|
|
15
|
+
const JPEG_QUALITY: float = 0.8
|
|
13
16
|
|
|
14
17
|
var _host: Node
|
|
15
18
|
var _values: GdhValues
|
|
@@ -385,16 +388,28 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
385
388
|
% screenshot_path.get_base_dir()
|
|
386
389
|
)
|
|
387
390
|
)
|
|
388
|
-
var
|
|
391
|
+
var answer: Dictionary = saved(image, screenshot_path)
|
|
392
|
+
answer.merge(described)
|
|
393
|
+
return answer
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
## [param image] saved as a PNG at [param png_path], and as a JPEG beside it, which the server sends
|
|
397
|
+
## instead when it is the smaller: a rendered frame is several times smaller as one, and every
|
|
398
|
+
## picture sent stays in the caller's session. Not for a picture with transparency, which a JPEG
|
|
399
|
+
## would flatten.
|
|
400
|
+
static func saved(image: Image, png_path: String) -> Dictionary:
|
|
401
|
+
var save_error: Error = image.save_png(png_path)
|
|
389
402
|
if save_error != OK:
|
|
390
403
|
return {"type": "error", "message": "Failed to save screenshot as PNG: " + str(save_error)}
|
|
391
|
-
|
|
392
404
|
var answer: Dictionary = {
|
|
393
405
|
"type": "screenshot_file",
|
|
394
406
|
"format": "png",
|
|
395
407
|
"width": image.get_width(),
|
|
396
408
|
"height": image.get_height(),
|
|
397
|
-
"path":
|
|
409
|
+
"path": png_path
|
|
398
410
|
}
|
|
399
|
-
|
|
411
|
+
if image.detect_alpha() == Image.ALPHA_NONE:
|
|
412
|
+
var jpeg_path: String = png_path.get_basename() + ".jpg"
|
|
413
|
+
if image.save_jpg(jpeg_path, JPEG_QUALITY) == OK:
|
|
414
|
+
answer["jpegPath"] = jpeg_path
|
|
400
415
|
return answer
|
|
@@ -524,7 +524,9 @@ static func _not_a_method_to_call(holder: Object, named: String, called: String)
|
|
|
524
524
|
% [called, named]
|
|
525
525
|
)
|
|
526
526
|
method = GdhPaths.method_of(named)
|
|
527
|
-
|
|
527
|
+
if holder.has_method(method):
|
|
528
|
+
return ""
|
|
529
|
+
return GdhPaths.with_methods_offered("%s has no method %s" % [called, method], holder, method)
|
|
528
530
|
|
|
529
531
|
|
|
530
532
|
## Whether a call's `properties` asks for its result to be read: true, or a list of names.
|
|
@@ -9,7 +9,7 @@ extends RefCounted
|
|
|
9
9
|
## platform when the pointer crosses into the window, sets it. An injected event never crosses
|
|
10
10
|
## anything, and on gdharness's own desktop no pointer ever enters a window, so nothing was hovered.
|
|
11
11
|
## A BaseButton takes a mouse press only while hovered, so every click on a button in such a window
|
|
12
|
-
## was dropped while the press itself was routed to it. Reported from
|
|
12
|
+
## was dropped while the press itself was routed to it. Reported from Game C, whose strip
|
|
13
13
|
## window holds its first-run question, and traced through Godot 4.7.2's source.
|
|
14
14
|
##
|
|
15
15
|
## What is supplied is what the engine does once the window has been entered: the control its own
|
|
@@ -157,6 +157,7 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
157
157
|
return {"type": "error", "message": holding}
|
|
158
158
|
var frames: int = holding
|
|
159
159
|
|
|
160
|
+
var focused: String = _focus_owner()
|
|
160
161
|
_say_action(action, whole or held, strength)
|
|
161
162
|
var lasted: Variant = null
|
|
162
163
|
if whole:
|
|
@@ -166,15 +167,34 @@ func inject_action(params: Dictionary) -> Dictionary:
|
|
|
166
167
|
_say_action(action, false, strength)
|
|
167
168
|
await _host.get_tree().process_frame
|
|
168
169
|
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
return _focus_noted(
|
|
171
|
+
{
|
|
172
|
+
"type": "input_injected",
|
|
173
|
+
"input_type": "action",
|
|
174
|
+
"action": action,
|
|
175
|
+
"pressed": held and not whole,
|
|
176
|
+
"whole": whole,
|
|
177
|
+
"held_frames": GdhValues.held_answer(whole, frames),
|
|
178
|
+
"held_for": lasted,
|
|
179
|
+
},
|
|
180
|
+
focused
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## The path of the Control holding the keyboard focus, or "" when none does. Read before a key or an
|
|
185
|
+
## action goes in, since pressing a focused button can move the focus.
|
|
186
|
+
func _focus_owner() -> String:
|
|
187
|
+
var holding: Control = _host.get_viewport().gui_get_focus_owner()
|
|
188
|
+
return "" if holding == null else str(holding.get_path())
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## [param said] with [param focused], the Control that had the focus, under focused. A key or an action
|
|
192
|
+
## reaches that Control before the game's _unhandled_input, so Space on a focused Button pressed it
|
|
193
|
+
## and the game's own handler counted the key as well, with nothing in the answer saying why.
|
|
194
|
+
static func _focus_noted(said: Dictionary, focused: String) -> Dictionary:
|
|
195
|
+
if not focused.is_empty():
|
|
196
|
+
said["focused"] = focused
|
|
197
|
+
return said
|
|
178
198
|
|
|
179
199
|
|
|
180
200
|
func _say_action(action: String, down: bool, strength: float) -> void:
|
|
@@ -230,6 +250,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
230
250
|
# keys are the code points themselves, which is what makes this a mapping and not a table.
|
|
231
251
|
event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
|
|
232
252
|
|
|
253
|
+
var focused: String = _focus_owner()
|
|
233
254
|
Input.parse_input_event(event)
|
|
234
255
|
var lasted: Variant = null
|
|
235
256
|
if whole:
|
|
@@ -239,20 +260,23 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
239
260
|
Input.parse_input_event(up)
|
|
240
261
|
await _host.get_tree().process_frame
|
|
241
262
|
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
return _focus_noted(
|
|
264
|
+
{
|
|
265
|
+
"type": "input_injected",
|
|
266
|
+
"input_type": "key",
|
|
267
|
+
"held_frames": GdhValues.held_answer(whole, frames),
|
|
268
|
+
"held_for": lasted,
|
|
269
|
+
"keycode": event.keycode,
|
|
270
|
+
"physical_keycode": event.physical_keycode,
|
|
271
|
+
"shift": event.shift_pressed,
|
|
272
|
+
"ctrl": event.ctrl_pressed,
|
|
273
|
+
"alt": event.alt_pressed,
|
|
274
|
+
"unicode": event.unicode,
|
|
275
|
+
"pressed": held and not whole,
|
|
276
|
+
"whole": whole,
|
|
277
|
+
},
|
|
278
|
+
focused
|
|
279
|
+
)
|
|
256
280
|
|
|
257
281
|
|
|
258
282
|
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
@@ -257,7 +257,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
257
257
|
## counted as unclean for a read.
|
|
258
258
|
##
|
|
259
259
|
## A step written in double quotes is the key inside them, so a key holding a colon is one step:
|
|
260
|
-
##
|
|
260
|
+
## Game D keys its stock by namespaced ids, "stock:\"base:dew\"", and split at that colon the
|
|
261
261
|
## step named a key "base" the map had never had.
|
|
262
262
|
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
263
263
|
var word: String = key_written(named)
|
|
@@ -416,7 +416,7 @@ static func _arity(object: Object, method: String) -> Dictionary:
|
|
|
416
416
|
static func call_refused(object: Object, method: String, called_as: String, given: int = 0) -> String:
|
|
417
417
|
var arity: Dictionary = _arity(object, method)
|
|
418
418
|
if arity.is_empty():
|
|
419
|
-
return "%s has no method %s" % [called_as, method]
|
|
419
|
+
return with_methods_offered("%s has no method %s" % [called_as, method], object, method)
|
|
420
420
|
var least: int = arity["least"]
|
|
421
421
|
var most: int = arity["most"]
|
|
422
422
|
var any_more: bool = arity["any_more"]
|
|
@@ -454,6 +454,32 @@ static func call_refused(object: Object, method: String, called_as: String, give
|
|
|
454
454
|
return ""
|
|
455
455
|
|
|
456
456
|
|
|
457
|
+
## [param said], a refusal for [param method], which [param object] lacks, with the methods it has
|
|
458
|
+
## nearest the name, or else those its script declares. The engine's own run to hundreds on a node,
|
|
459
|
+
## so they are offered only when one is near.
|
|
460
|
+
static func with_methods_offered(said: String, object: Object, method: String) -> String:
|
|
461
|
+
var own: Array[String] = []
|
|
462
|
+
var attached: Variant = object.get_script()
|
|
463
|
+
if attached is Script:
|
|
464
|
+
var script: Script = attached
|
|
465
|
+
for entry: Dictionary in script.get_script_method_list():
|
|
466
|
+
var name: String = str(entry.get("name", ""))
|
|
467
|
+
if not name.is_empty() and not own.has(name):
|
|
468
|
+
own.append(name)
|
|
469
|
+
var every: Array[String] = own.duplicate()
|
|
470
|
+
for entry: Dictionary in object.get_method_list():
|
|
471
|
+
var name: String = str(entry.get("name", ""))
|
|
472
|
+
if not every.has(name):
|
|
473
|
+
every.append(name)
|
|
474
|
+
var close: PackedStringArray = GdhRead.nearest(method, PackedStringArray(every))
|
|
475
|
+
if not close.is_empty():
|
|
476
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
477
|
+
if own.is_empty():
|
|
478
|
+
return said
|
|
479
|
+
own.sort()
|
|
480
|
+
return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(own))]
|
|
481
|
+
|
|
482
|
+
|
|
457
483
|
## The entry a script attached to [param object] declares for [param method], or an empty one when
|
|
458
484
|
## the method is the engine's.
|
|
459
485
|
static func _script_method(object: Object, method: String) -> Dictionary:
|
|
@@ -168,6 +168,16 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
168
168
|
var by_class: RegEx = null
|
|
169
169
|
if not class_name_declared.is_empty():
|
|
170
170
|
by_class = GdhPatterns.compiled("\\b" + _regex_escaped(class_name_declared) + "\\b")
|
|
171
|
+
# The script of an autoload is used wherever code names the autoload, and asked what uses it,
|
|
172
|
+
# the answer was its line in project.godot alone, beside the scripts calling it.
|
|
173
|
+
var autoload_named: String = ""
|
|
174
|
+
var autoloads: Dictionary = autoloads_declared()
|
|
175
|
+
for name: String in autoloads:
|
|
176
|
+
if str(autoloads[name]) == resource_path:
|
|
177
|
+
autoload_named = name
|
|
178
|
+
var by_autoload: RegEx = null
|
|
179
|
+
if not autoload_named.is_empty():
|
|
180
|
+
by_autoload = GdhPatterns.compiled("\\b" + _regex_escaped(autoload_named) + "\\b")
|
|
171
181
|
var target: Dictionary = {
|
|
172
182
|
"path": resource_path,
|
|
173
183
|
"uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
|
|
@@ -203,7 +213,12 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
203
213
|
# Strings are text, so a class named in one is not a use of it.
|
|
204
214
|
by_its_name = by_class.search(_code_of(line)) != null
|
|
205
215
|
name_in_comment = not by_its_name and by_class.search(_comment_of(line)) != null
|
|
206
|
-
|
|
216
|
+
var by_its_autoload: bool = false
|
|
217
|
+
if not by_its_path and not by_its_name and code and by_autoload != null:
|
|
218
|
+
by_its_autoload = by_autoload.search(_code_of(line)) != null
|
|
219
|
+
if not by_its_autoload and not name_in_comment:
|
|
220
|
+
name_in_comment = by_autoload.search(_comment_of(line)) != null
|
|
221
|
+
if not by_its_path and not by_its_name and not by_its_autoload and not name_in_comment:
|
|
207
222
|
continue
|
|
208
223
|
var trimmed: String = line.strip_edges()
|
|
209
224
|
var kind: String = ""
|
|
@@ -219,6 +234,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
219
234
|
kind = "comment"
|
|
220
235
|
elif by_its_path:
|
|
221
236
|
kind = _path_reference_kind(line, file_path)
|
|
237
|
+
elif by_its_autoload:
|
|
238
|
+
kind = "autoload"
|
|
222
239
|
else:
|
|
223
240
|
kind = (
|
|
224
241
|
"extends"
|
|
@@ -244,7 +261,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
244
261
|
var declared: Variant = null
|
|
245
262
|
if not class_name_declared.is_empty():
|
|
246
263
|
declared = class_name_declared
|
|
247
|
-
|
|
264
|
+
var answer: Dictionary = {
|
|
248
265
|
"resource_path": resource_path,
|
|
249
266
|
"exists": exists,
|
|
250
267
|
"class_name": declared,
|
|
@@ -260,6 +277,9 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
260
277
|
"by_kind": by_kind,
|
|
261
278
|
},
|
|
262
279
|
}
|
|
280
|
+
if not autoload_named.is_empty():
|
|
281
|
+
answer["autoload"] = autoload_named
|
|
282
|
+
return answer
|
|
263
283
|
|
|
264
284
|
|
|
265
285
|
# The class_name a script declares, or empty for a scene, a resource or a script without one.
|
|
@@ -477,15 +497,19 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
477
497
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
478
498
|
continue
|
|
479
499
|
|
|
480
|
-
_depend(deps, path, dep_path,
|
|
500
|
+
_depend(deps, path, dep_path, {}, current_depth, walk)
|
|
481
501
|
|
|
482
502
|
# A script uses another by its class_name as surely as by a preload, and only the reverse walk
|
|
483
503
|
# looked for that: asked what a script depends on, the answer left out the script it typed a
|
|
484
|
-
# variable with, while asking what used that script named this one.
|
|
504
|
+
# variable with, while asking what used that script named this one. An autoload's global
|
|
505
|
+
# name is the same kind of use, of the script registered under it.
|
|
485
506
|
if path.get_extension() == "gd":
|
|
486
507
|
var classes: Dictionary = classes_declared()
|
|
487
508
|
for used: String in classes_used_in(content, classes):
|
|
488
|
-
_depend(deps, path, str(classes[used]), used, current_depth, walk)
|
|
509
|
+
_depend(deps, path, str(classes[used]), {"by_class": used}, current_depth, walk)
|
|
510
|
+
var autoloads: Dictionary = autoloads_declared()
|
|
511
|
+
for used: String in classes_used_in(content, autoloads):
|
|
512
|
+
_depend(deps, path, str(autoloads[used]), {"by_autoload": used}, current_depth, walk)
|
|
489
513
|
|
|
490
514
|
walk.path_stack.pop_back()
|
|
491
515
|
walk.visited[path] = {"deps": deps, "remaining": remaining}
|
|
@@ -493,13 +517,13 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
493
517
|
|
|
494
518
|
|
|
495
519
|
# Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth left
|
|
496
|
-
# allows, once however many times it is named. [param
|
|
497
|
-
# or empty for a path.
|
|
520
|
+
# allows, once however many times it is named. [param named_by] is the class or autoload name it was
|
|
521
|
+
# named by, as by_class or by_autoload, or empty for a path.
|
|
498
522
|
func _depend(
|
|
499
523
|
deps: Array[Dictionary],
|
|
500
524
|
path: String,
|
|
501
525
|
dep_path: String,
|
|
502
|
-
|
|
526
|
+
named_by: Dictionary,
|
|
503
527
|
current_depth: int,
|
|
504
528
|
walk: GdhDependencyWalk
|
|
505
529
|
) -> void:
|
|
@@ -509,8 +533,7 @@ func _depend(
|
|
|
509
533
|
if existing.get("path", "") == dep_path:
|
|
510
534
|
return
|
|
511
535
|
var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
|
|
512
|
-
|
|
513
|
-
dep_info["by_class"] = by_class
|
|
536
|
+
dep_info.merge(named_by)
|
|
514
537
|
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
515
538
|
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
516
539
|
if sub_deps.size() > 0:
|
|
@@ -518,6 +541,22 @@ func _depend(
|
|
|
518
541
|
deps.append(dep_info)
|
|
519
542
|
|
|
520
543
|
|
|
544
|
+
## Every autoload the project registers as a global name, with its script. One that is not global
|
|
545
|
+
## is no name code can use.
|
|
546
|
+
static func autoloads_declared() -> Dictionary:
|
|
547
|
+
var autoloads: Dictionary = {}
|
|
548
|
+
for property: Dictionary in ProjectSettings.get_property_list():
|
|
549
|
+
var setting: String = str(property.get("name", ""))
|
|
550
|
+
if not setting.begins_with("autoload/"):
|
|
551
|
+
continue
|
|
552
|
+
var value: String = str(ProjectSettings.get_setting(setting))
|
|
553
|
+
if value.begins_with("*"):
|
|
554
|
+
autoloads[setting.trim_prefix("autoload/")] = _resolved(
|
|
555
|
+
value.trim_prefix("*"), "res://project.godot"
|
|
556
|
+
)
|
|
557
|
+
return autoloads
|
|
558
|
+
|
|
559
|
+
|
|
521
560
|
## Every class_name the project's scripts declare, with the script declaring it.
|
|
522
561
|
func classes_declared() -> Dictionary:
|
|
523
562
|
if _classes_read:
|
|
@@ -530,6 +569,18 @@ func classes_declared() -> Dictionary:
|
|
|
530
569
|
return _classes_declared
|
|
531
570
|
|
|
532
571
|
|
|
572
|
+
## Every class the project's scripts declare with class_name, sorted by name, with the script
|
|
573
|
+
## declaring it. Read from the files rather than the class cache, which only a scan rewrites.
|
|
574
|
+
func list_script_classes(_params: Dictionary) -> Dictionary:
|
|
575
|
+
var declared: Dictionary = classes_declared()
|
|
576
|
+
var names: Array = declared.keys()
|
|
577
|
+
names.sort()
|
|
578
|
+
var classes: Array[Dictionary] = []
|
|
579
|
+
for name: Variant in names:
|
|
580
|
+
classes.append({"name": str(name), "path": str(declared[name])})
|
|
581
|
+
return {"classes": classes, "count": classes.size()}
|
|
582
|
+
|
|
583
|
+
|
|
533
584
|
## The names in [param classes] that the code in [param content] uses, in the order first used: words
|
|
534
585
|
## outside strings and comments, so a class named in prose or in a message is not a use of it.
|
|
535
586
|
static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
|