gdharness 1.3.13 → 1.3.14

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 CHANGED
@@ -10896,7 +10896,13 @@ function exitNamed(code) {
10896
10896
  return `0x${unsigned.toString(16).toUpperCase()} (${status})`;
10897
10897
  }
10898
10898
  const gdunit = GDUNIT_EXITS.get(code);
10899
- return gdunit === undefined ? String(code) : `${String(code)} (${gdunit})`;
10899
+ if (gdunit !== undefined) {
10900
+ return `${String(code)} (${gdunit})`;
10901
+ }
10902
+ if (code > 0 && code < 256 && !GODOT_EXITS.has(code)) {
10903
+ return `${String(code)} (a code neither Godot nor gdUnit4 ends with, so most likely a quit(${String(code)}) in the project's code the engine ran)`;
10904
+ }
10905
+ return String(code);
10900
10906
  }
10901
10907
  function rowsAcross(engines) {
10902
10908
  const dealtOut = new Map;
@@ -11132,7 +11138,7 @@ ${String(error.warning)}`);
11132
11138
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
11133
11139
  return { printed: unexpected, neverPrinted };
11134
11140
  }
11135
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
11141
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11136
11142
  var init_suite_judge = __esm(() => {
11137
11143
  init_engine_leaks();
11138
11144
  init_junit();
@@ -11169,6 +11175,7 @@ var init_suite_judge = __esm(() => {
11169
11175
  [104, "gdUnit4's code for a Godot version it does not support"],
11170
11176
  [105, "gdUnit4's code for scripts that did not compile"]
11171
11177
  ]);
11178
+ GODOT_EXITS = new Set([1, 255]);
11172
11179
  });
11173
11180
 
11174
11181
  // src/engine-launch.ts
@@ -11247,16 +11254,24 @@ function resPathOf(entry, projectPath, cwd) {
11247
11254
  function homeOf(projectPath) {
11248
11255
  return mainCheckoutOf(projectPath) ?? projectPath;
11249
11256
  }
11250
- function sweepRunDirectories(runs) {
11257
+ function sweepRunDirectories(runs, kept = KEPT_RUN_DIRECTORIES) {
11251
11258
  if (!existsSync5(runs)) {
11252
11259
  return;
11253
11260
  }
11261
+ const ended = [];
11254
11262
  for (const entry of readdirSync3(runs)) {
11255
- const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
11256
- if (!Number.isInteger(pid) || !alive(pid)) {
11263
+ const named = /^(\d+)-(\d+)$/.exec(entry);
11264
+ const pid = Number(named?.[2]);
11265
+ if (!Number.isInteger(pid)) {
11257
11266
  rmSync4(join7(runs, entry), { recursive: true, force: true });
11267
+ } else if (!alive(pid)) {
11268
+ ended.push({ entry, started: Number(named?.[1]) });
11258
11269
  }
11259
11270
  }
11271
+ ended.sort((a, b) => b.started - a.started);
11272
+ for (const { entry } of ended.slice(kept)) {
11273
+ rmSync4(join7(runs, entry), { recursive: true, force: true });
11274
+ }
11260
11275
  }
11261
11276
  function roomFor(most, processors, available, said = "of memory free", processorsSaid = `the machine has ${plural(processors, "processor")}`) {
11262
11277
  const byMemory = Math.floor((available - MEMORY_KEPT) / MEMORY_PER_ENGINE);
@@ -11445,7 +11460,7 @@ function endTree(child) {
11445
11460
  process6.kill(-child.pid, "SIGKILL");
11446
11461
  } catch {}
11447
11462
  }
11448
- var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload";
11463
+ var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5;
11449
11464
  var init_engine_launch = __esm(() => {
11450
11465
  init_alive();
11451
11466
  init_desktop();
@@ -15995,7 +16010,7 @@ var init_tool_definitions = __esm(() => {
15995
16010
  {
15996
16011
  name: "project_settings",
15997
16012
  title: "Project settings",
15998
- description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16013
+ description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
15999
16014
  parameters: {
16000
16015
  projectPath: PROJECT_PATH,
16001
16016
  setting: {
@@ -17502,7 +17517,7 @@ var init_tool_definitions = __esm(() => {
17502
17517
  type: ["boolean", "array"],
17503
17518
  items: { type: "string" },
17504
17519
  ops: ["call"],
17505
- description: 'call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). A name that is not there is listed under result_properties_missing with why, and a result that is not an object answers properties_note instead. Without it an object comes back as its class and id only.'
17520
+ description: `call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). For a list or a dictionary the call answered with, they are read off each object in it: result_properties is then a list, or a dictionary under the same keys, with null for an element that is no object. A name that is not there is listed under result_properties_missing with why, led by the element's index or key for a list or dictionary, and a result with no object in it answers properties_note instead. Without it an object comes back as its class, its script_class or script, and id, which is the object's instance id (get_instance_id()) and never a property of its own named id: read that one through properties.`
17506
17521
  },
17507
17522
  timeoutMs: {
17508
17523
  type: "integer",
@@ -17912,7 +17927,7 @@ var init_tool_definitions = __esm(() => {
17912
17927
  {
17913
17928
  name: "debug_state",
17914
17929
  title: "Debugger state",
17915
- description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debugger's console output so far, its last thousand lines, with any earlier ones counted under notShown. For a game this server started the output is what the game printed; the errors and warnings it reported, with where each happened, are in editor_output. For a game this server started, its keeper's debugger answers: each frame names its script as the project spells it under script and on this machine under path, and held says which thread stopped and why. For a game the editor plays, the editor's debug adapter answers. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack. A function that awaited is resumed with no caller below it, since the call that started it returned at the await, so its frame is the only one. The variables give an object as its class and id only, and evaluate reads into it: an expression worked out by the game at a frame, with the frame's locals, the globals and every global class in scope and the frame's instance as self, such as wave.spawned or wave.spawned.size(). The game runs what the expression calls, so a call that changes something changes the game. A frame with no instance, a static function's called on its class, is refused, since the engine lets the game go on rather than evaluate there; the frame below it has one. The compiler's own locals, named with @ such as a for loop's @counter_pos, are left out.",
17930
+ description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debugger's console output so far, its last thousand lines, with any earlier ones counted under notShown. For a game this server started the output is what the game printed; the errors and warnings it reported, with where each happened, are in editor_output. For a game this server started, its keeper's debugger answers: each frame names its script as the project spells it under script and on this machine under path, and held says which thread stopped and why. For a game the editor plays, the editor's debug adapter answers. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack. A function that awaited is resumed with no caller below it, since the call that started it returned at the await, so its frame is the only one. The variables give an object as its class and instance id only, and evaluate reads into it: an expression worked out by the game at a frame, with the frame's locals, the globals and every global class in scope and the frame's instance as self, such as wave.spawned or wave.spawned.size(). The game runs what the expression calls, so a call that changes something changes the game. A frame with no instance, a static function's called on its class, is refused, since the engine lets the game go on rather than evaluate there; the frame below it has one. The compiler's own locals, named with @ such as a for loop's @counter_pos, are left out.",
17916
17931
  parameters: {
17917
17932
  frameId: {
17918
17933
  type: "number",
@@ -57961,7 +57976,7 @@ function failed(faults, engines, runDirectory, so) {
57961
57976
  for (const engine of engines) {
57962
57977
  console.log(`what ${engine.name} printed: ${engine.log}`);
57963
57978
  }
57964
- console.log(`Every engine's output is kept under ${runDirectory} until the next run.`);
57979
+ console.log(`Every engine's output is kept under ${runDirectory} until ${String(KEPT_RUN_DIRECTORIES)} later runs have failed.`);
57965
57980
  return 1;
57966
57981
  }
57967
57982
 
@@ -58748,7 +58763,7 @@ ${readFileSync22(engine.log, "utf8")}`);
58748
58763
  }
58749
58764
  const problems = failed.reduce((sum, engine) => sum + engine.verdict.faults.length, across.length + uncollected.length);
58750
58765
  console.log(`Failed: ${plural(problems, "problem")}, listed above. Ran ${totals}`);
58751
- console.log(`Every engine's output is kept under ${logs.shown} until the next run.`);
58766
+ console.log(`Every engine's output is kept under ${logs.shown} until ${String(KEPT_RUN_DIRECTORIES)} later runs have failed.`);
58752
58767
  return 1;
58753
58768
  }
58754
58769
  function writeEarlyResult(file, startedAt, problems, logs) {
@@ -694,12 +694,27 @@ static func _reads_the_result(asked: Variant) -> bool:
694
694
 
695
695
  ## What [param asked] reads off the object a call answered with: every variable its script declares
696
696
  ## when [param asked] is true, or the properties a list names, each a path walked from the object.
697
+ ## Off each object of a list or a dictionary the call answered with too, in the result's shape.
697
698
  ##
698
- ## In the same answer because an object handed back is otherwise only its class and an id, and
699
- ## reading one field of it meant finding the same object again by a path into the game's private
700
- ## state, which works only where the caller already knows a key. A name that is not there is listed
701
- ## rather than refusing the answer, since the call has run by then and its result is what was asked.
699
+ ## In the same answer because an object handed back is otherwise only its class and its instance
700
+ ## id, and reading one field of it meant finding the same object again by a path into the game's
701
+ ## private state, which works only where the caller already knows a key; a catalogue of records
702
+ ## handed back as a list took one call per record. A name that is not there is listed rather than
703
+ ## refusing the answer, since the call has run by then and its result is what was asked.
702
704
  func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
705
+ var missing: Array[String] = []
706
+ if result is Array:
707
+ var list: Array = result
708
+ var read_each: Array = []
709
+ for index: int in list.size():
710
+ read_each.append(_read_off_element(list[index], "[%d]" % index, asked, missing))
711
+ return _read_off_many(read_each, list, "Array", missing)
712
+ if result is Dictionary:
713
+ var map: Dictionary = result
714
+ var read_by_key: Dictionary = {}
715
+ for key: Variant in map:
716
+ read_by_key[str(key)] = _read_off_element(map[key], "[%s]" % str(key), asked, missing)
717
+ return _read_off_many(read_by_key, map.values(), "Dictionary", missing)
703
718
  if result is Object and not is_instance_valid(result):
704
719
  return {
705
720
  "properties_note": "the call answered an object that has since been freed, so nothing was read"
@@ -710,6 +725,49 @@ func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
710
725
  "the call answered %s, which has no properties to read" % type_string(typeof(result))
711
726
  }
712
727
  var object: Object = result
728
+ var one: Dictionary = _read_off_object(object, asked)
729
+ missing = one["missing"]
730
+ var answered: Dictionary = {"result_properties": one["read"]}
731
+ if not missing.is_empty():
732
+ answered["result_properties_missing"] = missing
733
+ return answered
734
+
735
+
736
+ ## What was read off one element of a list or a dictionary a call answered with, or null for one
737
+ ## that is no object; why a name was not there goes into [param missing], led by [param named].
738
+ func _read_off_element(element: Variant, named: String, asked: Variant, missing: Array[String]) -> Variant:
739
+ if not element is Object:
740
+ return null
741
+ if not is_instance_valid(element):
742
+ missing.append("%s is an object that has since been freed" % named)
743
+ return null
744
+ var object: Object = element
745
+ var one: Dictionary = _read_off_object(object, asked)
746
+ var whys: Array[String] = one["missing"]
747
+ for why: String in whys:
748
+ missing.append("%s %s" % [named, why])
749
+ return one["read"]
750
+
751
+
752
+ ## The answer for a list or a dictionary, [param read] holding what each element gave, unless none
753
+ ## of [param elements] is an object.
754
+ static func _read_off_many(
755
+ read: Variant, elements: Array, shape: String, missing: Array[String]
756
+ ) -> Dictionary:
757
+ if not elements.any(func(element: Variant) -> bool: return element is Object):
758
+ return {
759
+ "properties_note":
760
+ "the call answered %s with no object in it, so there are no properties to read" % shape
761
+ }
762
+ var answered: Dictionary = {"result_properties": read}
763
+ if not missing.is_empty():
764
+ answered["result_properties_missing"] = missing
765
+ return answered
766
+
767
+
768
+ ## What [param asked] reads off [param object], under read, and why each name it could not read is
769
+ ## not there, under missing.
770
+ func _read_off_object(object: Object, asked: Variant) -> Dictionary:
713
771
  var wanted: Array[String] = []
714
772
  if asked is Array:
715
773
  for name: Variant in asked:
@@ -724,10 +782,7 @@ func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
724
782
  missing.append(str(walked["message"]))
725
783
  else:
726
784
  read[path] = _values.serialize(walked["value"])
727
- var answered: Dictionary = {"result_properties": read}
728
- if not missing.is_empty():
729
- answered["result_properties_missing"] = missing
730
- return answered
785
+ return {"read": read, "missing": missing}
731
786
 
732
787
 
733
788
  ## The variables the script attached to [param object] declares, in the order it declares them.
@@ -892,10 +892,12 @@ class GdhTrace:
892
892
  elif value is Dictionary:
893
893
  var map: Dictionary = value
894
894
  _last = map.duplicate(true)
895
+ # Game time unrounded: a frame can be a tenth of a millisecond long, and a rate worked out over
896
+ # one from times cut to a ten-thousandth was off by its own size.
895
897
  var change: Dictionary = {
896
898
  "frame": frame,
897
899
  "elapsed_ms": elapsed,
898
- "game_time": snappedf(game_time, 0.0001),
900
+ "game_time": game_time,
899
901
  "value": values.serialize(value),
900
902
  }
901
903
  if changes.size() < MOST_CHANGES:
@@ -1,5 +1,5 @@
1
1
  extends Node
2
- ## Brings gdharness's runtime up in a debug build that has its addon, and does nothing anywhere else.
2
+ ## Brings gdharness's runtime up in a debug build that has its addon, and does nothing elsewhere.
3
3
  ##
4
4
  ## `project.godot` names this file rather than the addon's script. `addons/` is often not committed
5
5
  ## and exports leave it out, so an entry naming the addon boots a fresh clone or an exported build
@@ -5,10 +5,14 @@ extends RefCounted
5
5
  ## The engine writes each section's keys in the order it registered them, and an engine running a
6
6
  ## script registers them in another order than the editor does, so a one-setting change moved lines
7
7
  ## nobody touched across several sections and a commit of it read as a rewrite. The save is still the
8
- ## engine's, for what only it gets right: the header and each value's text. The order is taken from
9
- ## the file as it was, and so is every line of a key the call did not change: the engine leaves out
10
- ## each key at its default, so a project holding `unsafe_call_argument=0` on purpose lost the line to
11
- ## a change of another key.
8
+ ## engine's, for what only it gets right: the header and the text of each value the call changed.
9
+ ## The order is taken from the file as it was, and so is every line of a key the call did not change,
10
+ ## because the engine writes its settings as it holds them rather than as the file had them. It
11
+ ## leaves out each key at its default, so a project holding `unsafe_call_argument=0` on purpose lost
12
+ ## the line to a change of another key. It writes a dictionary with its keys sorted, so gdUnit4's
13
+ ## session hooks, which run in the order listed, changed order. And it writes what a
14
+ ## `project_settings_override` file laid over the project, so a player's window mode in the user
15
+ ## directory became the game's default.
12
16
 
13
17
  const PATH: String = "res://project.godot"
14
18
 
@@ -35,30 +39,48 @@ static func save(changed: PackedStringArray) -> Error:
35
39
  return OK
36
40
 
37
41
 
38
- ## [param written] with its sections and their keys put in the order [param before] has them, and
39
- ## each line [param before] has for a key [param written] lacks put back, unless that key's setting
40
- ## is in [param changed]. Every other line of the result is a line of [param written], so nothing the
41
- ## engine wrote is lost or changed.
42
+ ## [param before] with the lines of the settings in [param changed] as [param written] has them: one
43
+ ## the engine wrote goes in its place, or where the engine put it relative to its neighbours when it
44
+ ## is new, and one the engine left out leaves the file. Every other line is [param before]'s, in its
45
+ ## place, and a key the engine wrote that the file did not hold and the call did not change stays out.
46
+ ## The header is [param written]'s.
42
47
  static func in_order_of(before: String, written: String, changed: PackedStringArray) -> String:
43
48
  var old: Dictionary = _parsed(before)
44
49
  var fresh: Dictionary = _parsed(written)
45
50
  var old_sections: Array[Dictionary] = old["sections"]
46
- var fresh_sections: Array[Dictionary] = fresh["sections"]
51
+ var all_fresh: Array[Dictionary] = fresh["sections"]
52
+
53
+ var old_text: Dictionary[String, Dictionary] = {}
54
+ for section: Dictionary in old_sections:
55
+ var texts: Dictionary[String, String] = {}
56
+ for entry: Dictionary in section["entries"]:
57
+ texts[str(entry["key"])] = str(entry["text"])
58
+ old_text[str(section["name"])] = texts
47
59
 
48
60
  var by_name: Dictionary[String, Dictionary] = {}
49
61
  var fresh_names: Array[String] = []
62
+ var fresh_sections: Array[Dictionary] = []
50
63
  var entry_of: Dictionary[String, Dictionary] = {}
51
64
  var fresh_keys: Dictionary[String, Array] = {}
52
- for section: Dictionary in fresh_sections:
65
+ for section: Dictionary in all_fresh:
53
66
  var name: String = section["name"]
54
- by_name[name] = section
55
- fresh_names.append(name)
67
+ var held: Dictionary = old_text.get(name, {})
56
68
  var texts: Dictionary[String, String] = {}
57
69
  var keys: Array[String] = []
58
70
  for entry: Dictionary in section["entries"]:
59
71
  var key: String = entry["key"]
60
- texts[key] = str(entry["text"])
72
+ if changed.has(_setting_of(name, key)):
73
+ texts[key] = str(entry["text"])
74
+ elif held.has(key):
75
+ texts[key] = str(held[key])
76
+ else:
77
+ continue
61
78
  keys.append(key)
79
+ if keys.is_empty():
80
+ continue
81
+ by_name[name] = section
82
+ fresh_names.append(name)
83
+ fresh_sections.append(section)
62
84
  entry_of[name] = texts
63
85
  fresh_keys[name] = keys
64
86
 
package/build/keeper.js CHANGED
@@ -8331,6 +8331,7 @@ var GDUNIT_EXITS = new Map([
8331
8331
  [104, "gdUnit4's code for a Godot version it does not support"],
8332
8332
  [105, "gdUnit4's code for scripts that did not compile"]
8333
8333
  ]);
8334
+ var GODOT_EXITS = new Set([1, 255]);
8334
8335
 
8335
8336
  // src/windows-ask.ts
8336
8337
  var SCRIPT = [
@@ -8386,6 +8387,9 @@ async function linuxStartTimes(pids) {
8386
8387
  }
8387
8388
  return began;
8388
8389
  }
8390
+ async function linuxStartEarliest() {
8391
+ return 1000 + Math.ceil(1000 / (await ticksPerSecond() ?? 100));
8392
+ }
8389
8393
  var ticksAnswered;
8390
8394
  async function ticksPerSecond() {
8391
8395
  if (ticksAnswered !== undefined) {
@@ -9906,11 +9910,10 @@ import { setTimeout as delay2 } from "node:timers/promises";
9906
9910
  var RESTARTED_WITHIN_MS = 2000;
9907
9911
  var LIST_WITHIN_MS = 15000;
9908
9912
  var POLL_MS = 250;
9909
- function successorAmong(listed, exitedPid, engine, notBefore, exitedAt, secondsOnly) {
9910
- const slack = secondsOnly ? 1000 : 0;
9913
+ function successorAmong(listed, exitedPid, engine, notBefore, exitedAt, early) {
9911
9914
  const fold = (name) => process.platform === "win32" ? name.toLowerCase() : name;
9912
9915
  const wanted = fold(basename5(engine.replaceAll("\\", "/")));
9913
- const found = listed.filter((one) => one.pid !== exitedPid && fold(readCommandLine(one.command).executable) === wanted && one.startedAt + slack >= notBefore && one.startedAt + slack >= exitedAt - RESTARTED_WITHIN_MS);
9916
+ const found = listed.filter((one) => one.pid !== exitedPid && fold(readCommandLine(one.command).executable) === wanted && one.startedAt + early >= notBefore && one.startedAt + early >= exitedAt - RESTARTED_WITHIN_MS);
9914
9917
  return found.length === 1 ? found[0] ?? null : null;
9915
9918
  }
9916
9919
  async function followSuccessor(exitedPid, engine, notBefore, exitedAt, transcript) {
@@ -9981,7 +9984,7 @@ async function followOnWindows(exitedPid, engine, notBefore, exitedAt) {
9981
9984
  candidates.push({ pid: Number(fields[1]), startedAt: Number(fields[2]), command: fields[3].trim() });
9982
9985
  }
9983
9986
  }
9984
- const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, false);
9987
+ const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, 0);
9985
9988
  holder.stdin.end(successor === null ? `
9986
9989
  ` : `${String(successor.pid)}
9987
9990
  `);
@@ -9996,6 +9999,7 @@ async function followOnWindows(exitedPid, engine, notBefore, exitedAt) {
9996
9999
  }
9997
10000
  async function followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript) {
9998
10001
  const linux = process.platform === "linux";
10002
+ const early = linux ? await linuxStartEarliest() : 1000;
9999
10003
  let printed;
10000
10004
  try {
10001
10005
  ({ stdout: printed } = await runHidden("ps", ["-e", "-o", linux ? "pid=,args=" : "pid=,lstart=,args="], {
@@ -10029,7 +10033,7 @@ async function followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript)
10029
10033
  }
10030
10034
  }
10031
10035
  }
10032
- const timely = listed.filter((one) => successorAmong([one], exitedPid, engine, notBefore, exitedAt, true) !== null);
10036
+ const timely = listed.filter((one) => successorAmong([one], exitedPid, engine, notBefore, exitedAt, early) !== null);
10033
10037
  const ours = canonical(transcript);
10034
10038
  const candidates = [];
10035
10039
  for (const one of timely) {
@@ -10038,7 +10042,7 @@ async function followOnPosix(exitedPid, engine, notBefore, exitedAt, transcript)
10038
10042
  candidates.push(one);
10039
10043
  }
10040
10044
  }
10041
- const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, true);
10045
+ const successor = successorAmong(candidates, exitedPid, engine, notBefore, exitedAt, early);
10042
10046
  if (successor === null) {
10043
10047
  return null;
10044
10048
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
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",