gdharness 1.3.58 → 1.3.60

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.
@@ -40,6 +40,7 @@ const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_
40
40
  ## Control's layout_mode values, which the engine does not expose as constants.
41
41
  const LAYOUT_MODE_POSITION: int = 0
42
42
  const LAYOUT_MODE_ANCHORS: int = 1
43
+ const LAYOUT_MODE_UNCONTROLLED: int = 3
43
44
  ## What a container sets on each child it lays out, beside the anchors and offsets.
44
45
  const PLACED_BY_A_CONTAINER: Array[String] = ["position", "size", "global_position"]
45
46
 
@@ -366,8 +367,14 @@ func _laid_out(control: Control, name: String) -> String:
366
367
  )
367
368
  % [control.name, parent.name, parent.get_class(), base]
368
369
  )
369
- if anchoring and GdhRead.as_int(control.get("layout_mode")) == LAYOUT_MODE_POSITION:
370
+ var mode: int = GdhRead.as_int(control.get("layout_mode"))
371
+ if anchoring and mode == LAYOUT_MODE_POSITION:
370
372
  control.set("layout_mode", LAYOUT_MODE_ANCHORS)
373
+ # A scene's root reads as Uncontrolled but keeps the Position mode it was made with until it
374
+ # enters a tree, and the engine ignores a preset in that stored mode, so full rect read back as
375
+ # top left on every root a headless edit wrote.
376
+ elif anchoring and mode == LAYOUT_MODE_UNCONTROLLED:
377
+ control.set("layout_mode", LAYOUT_MODE_UNCONTROLLED)
371
378
  return ""
372
379
 
373
380
 
@@ -16,6 +16,13 @@ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
16
16
 
17
17
  if result["exists"]:
18
18
  var value: Variant = ProjectSettings.get_setting(setting_path)
19
+ var laid: Dictionary = laid_over(setting_path, override_files())
20
+ if not laid.is_empty():
21
+ if not Engine.is_editor_hint():
22
+ value = laid["project"]
23
+ result["overridden_in"] = {
24
+ "file": laid["file"], "value": values.serialize_value(laid["value"]),
25
+ }
19
26
  result["value"] = values.serialize_value(value)
20
27
  # A setting the engine registers exists whether or not the project sets it, so exists did
21
28
  # not say whether project.godot has a line for it. At its default it has none: the engine
@@ -54,17 +61,25 @@ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
54
61
  ## wrong: 2 is as true as true is.
55
62
  static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
56
63
  var found: Array[Dictionary] = []
64
+ var overrides: Array[Dictionary] = override_files()
57
65
  for property: Dictionary in ProjectSettings.get_property_list():
58
66
  var name: String = str(property.get("name", ""))
59
67
  if not name.begins_with(prefix):
60
68
  continue
61
69
  var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
62
70
  var value: Variant = ProjectSettings.get_setting(name)
71
+ var laid: Dictionary = laid_over(name, overrides)
72
+ if not laid.is_empty() and not Engine.is_editor_hint():
73
+ value = laid["project"]
63
74
  var described: Dictionary = {
64
75
  "setting": name,
65
76
  "type": type_named(kind, value),
66
77
  "value": values.serialize_value(value),
67
78
  }
79
+ if not laid.is_empty():
80
+ described["overridden_in"] = {
81
+ "file": laid["file"], "value": values.serialize_value(laid["value"]),
82
+ }
68
83
  # As a single get says it, so a family read can be told apart into what the project sets and
69
84
  # the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
70
85
  var default_value: Variant = ProjectSettings.property_get_revert(name)
@@ -81,6 +96,49 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
81
96
  return { "prefix": prefix, "count": found.size(), "settings": found }
82
97
 
83
98
 
99
+ ## The files a run lays over project.godot, loaded, in the order the engine lays them, each as
100
+ ## {"file", "config"}: res://override.cfg, then the file
101
+ ## application/config/project_settings_override
102
+ ## names. The editor lays neither, so a game keeping a player's window size in user:// had that size
103
+ ## answered headless as the project's own setting, while the editor answered the project's.
104
+ static func override_files() -> Array[Dictionary]:
105
+ var files: Array[Dictionary] = []
106
+ var named: String = str(
107
+ ProjectSettings.get_setting("application/config/project_settings_override", "")
108
+ )
109
+ for file: String in ["res://override.cfg", named]:
110
+ var config: ConfigFile = ConfigFile.new()
111
+ if not file.is_empty() and config.load(file) == OK:
112
+ files.append({ "file": file, "config": config })
113
+ return files
114
+
115
+
116
+ ## What the last of [param overrides], from [method override_files], to set [param setting_path]
117
+ ## sets it to, as {"file", "value", "project"}, where project is what project.godot holds, or the
118
+ ## engine's default where it has no line: the value the editor holds, unsaved changes aside. Empty
119
+ ## when none sets it.
120
+ static func laid_over(setting_path: String, overrides: Array[Dictionary]) -> Dictionary:
121
+ var at: int = setting_path.find("/")
122
+ if at <= 0:
123
+ return {}
124
+ var section: String = setting_path.left(at)
125
+ var key: String = setting_path.substr(at + 1)
126
+ for index: int in range(overrides.size() - 1, -1, -1):
127
+ var config: ConfigFile = overrides[index]["config"]
128
+ if not config.has_section_key(section, key):
129
+ continue
130
+ var project: ConfigFile = ConfigFile.new()
131
+ var held: Variant = ProjectSettings.property_get_revert(setting_path)
132
+ if project.load("res://project.godot") == OK and project.has_section_key(section, key):
133
+ held = project.get_value(section, key)
134
+ return {
135
+ "file": overrides[index]["file"],
136
+ "value": config.get_value(section, key),
137
+ "project": held,
138
+ }
139
+ return {}
140
+
141
+
84
142
  ## The type [param kind] names, with what [param value] holds when it is a typed array or
85
143
  ## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
86
144
  ## which hides what its keys and values must be.
@@ -804,12 +804,15 @@ static func with_methods_offered(said: String, object: Object, method: String) -
804
804
  # to a refusal.
805
805
  var flags: int = entry.get("flags", 0)
806
806
  var callable: bool = not object is Script or flags & METHOD_FLAG_STATIC
807
- if callable and not name.is_empty() and not own.has(name):
807
+ # A property's setter and getter are named by the engine, @asked_setter, and offered
808
+ # they
809
+ # crowded out the script's own methods with names nobody means.
810
+ if callable and not name.begins_with("@") and not name.is_empty() and not own.has(name):
808
811
  own.append(name)
809
812
  var every: Array[String] = own.duplicate()
810
813
  for entry: Dictionary in GdhValues.methods_of(object):
811
814
  var name: String = str(entry.get("name", ""))
812
- if not every.has(name):
815
+ if not name.begins_with("@") and not every.has(name):
813
816
  every.append(name)
814
817
  var close: PackedStringArray = GdhRead.nearest(method, PackedStringArray(every))
815
818
  if not close.is_empty():
@@ -89,8 +89,13 @@ func set_project_setting(params: Dictionary) -> Dictionary:
89
89
 
90
90
  var old_value: Variant = null
91
91
  var had_value: bool = ProjectSettings.has_setting(setting_path)
92
+ var laid: Dictionary = GdhSettingReads.laid_over(
93
+ setting_path, GdhSettingReads.override_files()
94
+ )
92
95
  if had_value:
93
- old_value = ProjectSettings.get_setting(setting_path)
96
+ old_value = (
97
+ ProjectSettings.get_setting(setting_path) if laid.is_empty() else laid["project"]
98
+ )
94
99
  # A setting the engine registers is there with its default whether or not project.godot sets it,
95
100
  # so has_setting alone called the description of a project that had never set one an old value.
96
101
  # Whether it is new is the file's to say: one at its default has a line only where the project
@@ -131,6 +136,16 @@ func set_project_setting(params: Dictionary) -> Dictionary:
131
136
  "was_default": was_default,
132
137
  "saved": true,
133
138
  }
139
+ if not laid.is_empty():
140
+ var read: Variant = _values.serialize_value(laid["value"])
141
+ answer["overridden_in"] = { "file": laid["file"], "value": read }
142
+ answer["overriddenNote"] = (
143
+ (
144
+ "A run on this machine still reads %s for it from %s, which a run lays over "
145
+ + "project.godot and the editor does not."
146
+ )
147
+ % [JSON.stringify(read), laid["file"]]
148
+ )
134
149
  # The same value in the same type is no change, and the file is left alone: a save rewrites it
135
150
  # whole. The type is compared as well, so an int setting the file holds as 50.0 is still put
136
151
  # right.
@@ -254,6 +269,11 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
254
269
  var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
255
270
  var held: bool = ProjectSettings.has_setting(setting_path)
256
271
  var value: Variant = ProjectSettings.get_setting(setting_path) if held else null
272
+ var laid: Dictionary = GdhSettingReads.laid_over(
273
+ setting_path, GdhSettingReads.override_files()
274
+ )
275
+ if held and not laid.is_empty():
276
+ value = laid["project"]
257
277
  var answer: Dictionary = {
258
278
  "setting_path": setting_path,
259
279
  "default_value": _values.serialize_value(default_value) if default_value != null else null,
@@ -40,6 +40,7 @@ const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_
40
40
  ## Control's layout_mode values, which the engine does not expose as constants.
41
41
  const LAYOUT_MODE_POSITION: int = 0
42
42
  const LAYOUT_MODE_ANCHORS: int = 1
43
+ const LAYOUT_MODE_UNCONTROLLED: int = 3
43
44
  ## What a container sets on each child it lays out, beside the anchors and offsets.
44
45
  const PLACED_BY_A_CONTAINER: Array[String] = ["position", "size", "global_position"]
45
46
 
@@ -366,8 +367,14 @@ func _laid_out(control: Control, name: String) -> String:
366
367
  )
367
368
  % [control.name, parent.name, parent.get_class(), base]
368
369
  )
369
- if anchoring and GdhRead.as_int(control.get("layout_mode")) == LAYOUT_MODE_POSITION:
370
+ var mode: int = GdhRead.as_int(control.get("layout_mode"))
371
+ if anchoring and mode == LAYOUT_MODE_POSITION:
370
372
  control.set("layout_mode", LAYOUT_MODE_ANCHORS)
373
+ # A scene's root reads as Uncontrolled but keeps the Position mode it was made with until it
374
+ # enters a tree, and the engine ignores a preset in that stored mode, so full rect read back as
375
+ # top left on every root a headless edit wrote.
376
+ elif anchoring and mode == LAYOUT_MODE_UNCONTROLLED:
377
+ control.set("layout_mode", LAYOUT_MODE_UNCONTROLLED)
371
378
  return ""
372
379
 
373
380
 
@@ -16,6 +16,13 @@ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
16
16
 
17
17
  if result["exists"]:
18
18
  var value: Variant = ProjectSettings.get_setting(setting_path)
19
+ var laid: Dictionary = laid_over(setting_path, override_files())
20
+ if not laid.is_empty():
21
+ if not Engine.is_editor_hint():
22
+ value = laid["project"]
23
+ result["overridden_in"] = {
24
+ "file": laid["file"], "value": values.serialize_value(laid["value"]),
25
+ }
19
26
  result["value"] = values.serialize_value(value)
20
27
  # A setting the engine registers exists whether or not the project sets it, so exists did
21
28
  # not say whether project.godot has a line for it. At its default it has none: the engine
@@ -54,17 +61,25 @@ static func one(setting_path: String, values: GdhSerialisation) -> Dictionary:
54
61
  ## wrong: 2 is as true as true is.
55
62
  static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
56
63
  var found: Array[Dictionary] = []
64
+ var overrides: Array[Dictionary] = override_files()
57
65
  for property: Dictionary in ProjectSettings.get_property_list():
58
66
  var name: String = str(property.get("name", ""))
59
67
  if not name.begins_with(prefix):
60
68
  continue
61
69
  var kind: int = GdhRead.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
62
70
  var value: Variant = ProjectSettings.get_setting(name)
71
+ var laid: Dictionary = laid_over(name, overrides)
72
+ if not laid.is_empty() and not Engine.is_editor_hint():
73
+ value = laid["project"]
63
74
  var described: Dictionary = {
64
75
  "setting": name,
65
76
  "type": type_named(kind, value),
66
77
  "value": values.serialize_value(value),
67
78
  }
79
+ if not laid.is_empty():
80
+ described["overridden_in"] = {
81
+ "file": laid["file"], "value": values.serialize_value(laid["value"]),
82
+ }
68
83
  # As a single get says it, so a family read can be told apart into what the project sets and
69
84
  # the engine's defaults: "input/" is ninety actions the engine ships and the project's few.
70
85
  var default_value: Variant = ProjectSettings.property_get_revert(name)
@@ -81,6 +96,49 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
81
96
  return { "prefix": prefix, "count": found.size(), "settings": found }
82
97
 
83
98
 
99
+ ## The files a run lays over project.godot, loaded, in the order the engine lays them, each as
100
+ ## {"file", "config"}: res://override.cfg, then the file
101
+ ## application/config/project_settings_override
102
+ ## names. The editor lays neither, so a game keeping a player's window size in user:// had that size
103
+ ## answered headless as the project's own setting, while the editor answered the project's.
104
+ static func override_files() -> Array[Dictionary]:
105
+ var files: Array[Dictionary] = []
106
+ var named: String = str(
107
+ ProjectSettings.get_setting("application/config/project_settings_override", "")
108
+ )
109
+ for file: String in ["res://override.cfg", named]:
110
+ var config: ConfigFile = ConfigFile.new()
111
+ if not file.is_empty() and config.load(file) == OK:
112
+ files.append({ "file": file, "config": config })
113
+ return files
114
+
115
+
116
+ ## What the last of [param overrides], from [method override_files], to set [param setting_path]
117
+ ## sets it to, as {"file", "value", "project"}, where project is what project.godot holds, or the
118
+ ## engine's default where it has no line: the value the editor holds, unsaved changes aside. Empty
119
+ ## when none sets it.
120
+ static func laid_over(setting_path: String, overrides: Array[Dictionary]) -> Dictionary:
121
+ var at: int = setting_path.find("/")
122
+ if at <= 0:
123
+ return {}
124
+ var section: String = setting_path.left(at)
125
+ var key: String = setting_path.substr(at + 1)
126
+ for index: int in range(overrides.size() - 1, -1, -1):
127
+ var config: ConfigFile = overrides[index]["config"]
128
+ if not config.has_section_key(section, key):
129
+ continue
130
+ var project: ConfigFile = ConfigFile.new()
131
+ var held: Variant = ProjectSettings.property_get_revert(setting_path)
132
+ if project.load("res://project.godot") == OK and project.has_section_key(section, key):
133
+ held = project.get_value(section, key)
134
+ return {
135
+ "file": overrides[index]["file"],
136
+ "value": config.get_value(section, key),
137
+ "project": held,
138
+ }
139
+ return {}
140
+
141
+
84
142
  ## The type [param kind] names, with what [param value] holds when it is a typed array or
85
143
  ## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
86
144
  ## which hides what its keys and values must be.
@@ -549,7 +549,25 @@ class GdhRunner:
549
549
  return
550
550
  if Engine.is_embedded_in_editor():
551
551
  return
552
- if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_MINIMIZED:
552
+ var mode: DisplayServer.WindowMode = DisplayServer.window_get_mode()
553
+ if mode == DisplayServer.WINDOW_MODE_MAXIMIZED and not DisplayServer.window_can_draw():
554
+ # On macOS Godot reads a window whose frame fills the usable screen as maximised before
555
+ # asking whether it is minimised, and changing the mode from that reading unzooms it, so
556
+ # a minimised window that size, as a small screen makes the default one, stayed
557
+ # minimised. A point off it reads as what it is, and goes back once restored. Godot
558
+ # divides pixels by the scale into whole points, so a pixel moves nothing on a scaled
559
+ # screen.
560
+ var at: Vector2i = DisplayServer.window_get_position()
561
+ DisplayServer.window_set_position(
562
+ at + Vector2i(ceili(DisplayServer.screen_get_max_scale()), 0)
563
+ )
564
+ mode = DisplayServer.window_get_mode()
565
+ if mode == DisplayServer.WINDOW_MODE_MINIMIZED:
566
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
567
+ minimise_pending = false
568
+ DisplayServer.window_set_position(at)
569
+ return
570
+ if mode == DisplayServer.WINDOW_MODE_MINIMIZED:
553
571
  DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
554
572
  minimise_pending = false
555
573
 
package/build/keeper.js CHANGED
@@ -6946,7 +6946,7 @@ var require_public_api = __commonJS(function(exports) {
6946
6946
  });
6947
6947
 
6948
6948
  // src/keeper.ts
6949
- import { closeSync as closeSync3, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync4, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync4 } from "node:fs";
6949
+ import { closeSync as closeSync3, existsSync as existsSync4, mkdtempSync as mkdtempSync4, openSync as openSync4, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync4 } from "node:fs";
6950
6950
  import { tmpdir as tmpdir8 } from "node:os";
6951
6951
  import { join as join10 } from "node:path";
6952
6952
  import { setTimeout as delay3 } from "node:timers/promises";
@@ -7435,6 +7435,7 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
7435
7435
  // src/engine-slots.ts
7436
7436
  import {
7437
7437
  closeSync as closeSync2,
7438
+ existsSync as existsSync2,
7438
7439
  mkdirSync,
7439
7440
  openSync as openSync2,
7440
7441
  readdirSync,
@@ -9338,7 +9339,7 @@ var IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
9338
9339
  var hashes = new Map;
9339
9340
 
9340
9341
  // src/scratch.ts
9341
- import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9342
+ import { existsSync as existsSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9342
9343
  import { tmpdir as tmpdir5 } from "node:os";
9343
9344
  import { join as join4 } from "node:path";
9344
9345
  import process6 from "node:process";
@@ -9383,7 +9384,7 @@ function tryAgain(path, attempt) {
9383
9384
  lastTryOnExit();
9384
9385
  }
9385
9386
  const after = AGAIN_AFTER_MS[attempt];
9386
- if (after === undefined || !existsSync2(path)) {
9387
+ if (after === undefined || !existsSync3(path)) {
9387
9388
  waiting.delete(path);
9388
9389
  if (after === undefined) {
9389
9390
  atExit.add(path);
@@ -10888,7 +10889,7 @@ async function startThroughHelper(spec, output, stopFile) {
10888
10889
  exited.now = true;
10889
10890
  });
10890
10891
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
10891
- const pid = existsSync3(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10892
+ const pid = existsSync4(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10892
10893
  if (Number.isInteger(pid) && pid > 0) {
10893
10894
  return { child: started.child, pid };
10894
10895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.58",
3
+ "version": "1.3.60",
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",