gdharness 1.3.41 → 1.3.42

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.
@@ -100,6 +100,9 @@ func create_gdscript(params: Dictionary) -> Dictionary:
100
100
  file.close()
101
101
  if not stored:
102
102
  return _log.failure("Failed to write to script file: " + full_script_path)
103
+ # The editor gives a script it makes a .uid beside it. A headless engine writes none until the
104
+ # project is imported, so a script made here was missing from every check tracking the sidecars.
105
+ var uid: String = _uid_beside(full_script_path)
103
106
 
104
107
  var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
105
108
  var parses: bool = _parses(written)
@@ -110,7 +113,10 @@ func create_gdscript(params: Dictionary) -> Dictionary:
110
113
  "absolute_path": ProjectSettings.globalize_path(full_script_path),
111
114
  "template_used": template if not template.is_empty() else "none",
112
115
  "parses": parses,
116
+ "uid": null,
113
117
  }
118
+ if not uid.is_empty():
119
+ answer["uid"] = uid
114
120
  # Read off the file as the engine parsed it rather than off the arguments, which content leaves
115
121
  # out. A script that does not parse tells the engine nothing, and its empty base and name were
116
122
  # answered for a file whose header says otherwise, so those are read off the text instead.
@@ -125,6 +131,22 @@ func create_gdscript(params: Dictionary) -> Dictionary:
125
131
  return answer
126
132
 
127
133
 
134
+ ## The uid in the .uid file beside [param path], written first when there is none, as the editor
135
+ ## writes it for a script it makes; empty when it could not be written. A sidecar already there is
136
+ ## the one the engine reads the script's uid from, so it is kept.
137
+ static func _uid_beside(path: String) -> String:
138
+ var sidecar: String = path + ".uid"
139
+ if FileAccess.file_exists(sidecar):
140
+ return FileAccess.get_file_as_string(sidecar).strip_edges()
141
+ var text: String = ResourceUID.id_to_text(ResourceUID.create_id_for_path(path))
142
+ var file: FileAccess = FileAccess.open(sidecar, FileAccess.WRITE)
143
+ if file == null:
144
+ return ""
145
+ var stored: bool = file.store_string(text + "\n")
146
+ file.close()
147
+ return text if stored else ""
148
+
149
+
128
150
  ## The refusal for [param path], which is no GDScript file, naming what [param doing] it instead:
129
151
  ## a shader is made with resource_edit, and once it exists its text is edited as a file's.
130
152
  static func _not_gdscript(path: String, doing: String) -> String:
@@ -640,9 +662,24 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
640
662
 
641
663
  # The body's statements relative to the function, whether or not the caller indented them for
642
664
  # it: a body written with its tab already on came out two levels in. A blank line stays empty,
643
- # since an indent alone on it is trailing whitespace to every formatter.
644
- for bl: String in body.dedent().split("\n"):
645
- func_lines.append("" if bl.strip_edges().is_empty() else "\t" + bl)
665
+ # since an indent alone on it is trailing whitespace to every formatter. Each level is the
666
+ # file's own unit, because the engine refuses a file indented with spaces in one place and tabs
667
+ # in another, so a tab put into a script indented with spaces stopped it parsing.
668
+ var unit: String = _indent_unit(lines)
669
+ var body_lines: Array[String] = []
670
+ body_lines.assign(body.dedent().strip_edges(false, true).split("\n"))
671
+ var given_unit: String = _indent_unit(body_lines)
672
+ for bl: String in body_lines:
673
+ if bl.strip_edges().is_empty():
674
+ func_lines.append("")
675
+ continue
676
+ var stripped: String = bl.lstrip(" \t")
677
+ var leading: String = bl.substr(0, bl.length() - stripped.length())
678
+ var levels: int = floori(float(leading.length()) / given_unit.length())
679
+ if leading != given_unit.repeat(levels):
680
+ func_lines.append(unit + bl)
681
+ continue
682
+ func_lines.append(unit.repeat(levels + 1) + stripped)
646
683
 
647
684
  var below: int = _blank_run(lines, insert_line, 1)
648
685
  if insert_line + below < lines.size():
@@ -654,6 +691,21 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
654
691
  return declared_at if _insert(lines, insert_line, func_lines, placed) else -1
655
692
 
656
693
 
694
+ ## One level of indentation in [param lines]: what the first line indented below an unindented
695
+ ## line ending a block opens with, or a tab, the engine's default, when no line is.
696
+ static func _indent_unit(lines: Array[String]) -> String:
697
+ var opener: bool = false
698
+ for line: String in lines:
699
+ var stripped: String = line.strip_edges()
700
+ if stripped.is_empty() or stripped.begins_with("#"):
701
+ continue
702
+ var leading: String = line.substr(0, line.length() - line.lstrip(" \t").length())
703
+ if opener and not leading.is_empty():
704
+ return leading
705
+ opener = leading.is_empty() and stripped.ends_with(":")
706
+ return "\t"
707
+
708
+
657
709
  # How many blank lines run from [param from] on in the direction of [param step], 1 or -1.
658
710
  static func _blank_run(lines: Array[String], from: int, step: int) -> int:
659
711
  var count: int = 0
@@ -280,6 +280,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
280
280
  "deadzone": deadzone,
281
281
  "events": events_config,
282
282
  "was_new": not ProjectSettings.has_setting(setting),
283
+ "saved": true,
283
284
  }
284
285
  if not answer["was_new"]:
285
286
  var old_events: Array[Dictionary] = []
@@ -7,6 +7,52 @@ const GdhProjectFile = preload("project_file.gd")
7
7
  const GdhSerialisation = preload("serialisation.gd")
8
8
  const GdhSettingReads = preload("setting_reads.gd")
9
9
  const GdhShapes = preload("value_shapes.gd")
10
+ ## The words GDScript reserves, which no script could reach an autoload by.
11
+ const GDSCRIPT_KEYWORDS: Array[String] = [
12
+ "and",
13
+ "as",
14
+ "assert",
15
+ "await",
16
+ "break",
17
+ "breakpoint",
18
+ "class",
19
+ "class_name",
20
+ "const",
21
+ "continue",
22
+ "elif",
23
+ "else",
24
+ "enum",
25
+ "extends",
26
+ "false",
27
+ "for",
28
+ "func",
29
+ "if",
30
+ "in",
31
+ "INF",
32
+ "is",
33
+ "match",
34
+ "NAN",
35
+ "namespace",
36
+ "not",
37
+ "null",
38
+ "or",
39
+ "pass",
40
+ "PI",
41
+ "preload",
42
+ "return",
43
+ "self",
44
+ "signal",
45
+ "static",
46
+ "super",
47
+ "TAU",
48
+ "trait",
49
+ "true",
50
+ "var",
51
+ "void",
52
+ "when",
53
+ "while",
54
+ "yield",
55
+ ]
10
56
 
11
57
  var _log: GdhLog
12
58
  var _values: GdhSerialisation = GdhSerialisation.new()
@@ -133,6 +179,23 @@ func set_project_setting(params: Dictionary) -> Dictionary:
133
179
  var err: Error = GdhProjectFile.save([setting_path])
134
180
  if err != OK:
135
181
  return _log.failure("Failed to save project.godot: " + error_string(err))
182
+ # An override still replaces what was written wherever its feature is, and a write of the base
183
+ # value read as if it reached every build.
184
+ var overrides: Dictionary = GdhSettingReads.overrides_of(setting_path, _values)
185
+ if not overrides.is_empty():
186
+ answer["overrides"] = overrides
187
+ var parts: Array[String] = []
188
+ for feature: Variant in overrides:
189
+ parts.append(
190
+ (
191
+ "one with the feature %s reads %s from %s.%s"
192
+ % [feature, JSON.stringify(overrides[feature]), setting_path, feature]
193
+ )
194
+ )
195
+ answer["overridesNote"] = (
196
+ "Not every build reads what was written: %s. Set or unset an override to change what its builds read."
197
+ % "; ".join(parts)
198
+ )
136
199
  return answer
137
200
 
138
201
 
@@ -321,6 +384,10 @@ func add_autoload(params: Dictionary) -> Dictionary:
321
384
  )
322
385
  )
323
386
 
387
+ var clash: String = _autoload_name_refusal(name)
388
+ if not clash.is_empty():
389
+ return _log.failure(clash)
390
+
324
391
  if not path.begins_with("res://"):
325
392
  path = "res://" + path
326
393
 
@@ -364,6 +431,32 @@ func add_autoload(params: Dictionary) -> Dictionary:
364
431
  }
365
432
 
366
433
 
434
+ ## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
435
+ ## the same names: an autoload named after an engine class or a built-in type takes that name in
436
+ ## every script, so an autoload called Input made every Input.is_action_pressed call the autoload.
437
+ static func _autoload_name_refusal(name: String) -> String:
438
+ var reason: String = ""
439
+ if not name.is_valid_ascii_identifier():
440
+ reason = "it is not an identifier, which a script could name"
441
+ elif name in GDSCRIPT_KEYWORDS:
442
+ reason = "it is a GDScript keyword"
443
+ elif ClassDB.class_exists(name):
444
+ reason = (
445
+ "it is the engine class %s, and every script naming %s would reach the autoload instead"
446
+ % [name, name]
447
+ )
448
+ else:
449
+ for type: int in TYPE_MAX:
450
+ if type_string(type) == name:
451
+ reason = "it is the built-in type %s" % name
452
+ if reason.is_empty():
453
+ return ""
454
+ return (
455
+ "%s cannot name an autoload: %s. Nothing was written; the editor's Autoload tab refuses it too."
456
+ % [JSON.stringify(name), reason]
457
+ )
458
+
459
+
367
460
  # Remove an autoload singleton
368
461
  func remove_autoload(params: Dictionary) -> Dictionary:
369
462
  var name: String = str(params.get("name", ""))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.41",
3
+ "version": "1.3.42",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",