gdharness 1.3.57 → 1.3.59

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
@@ -1763,6 +1763,9 @@ function nonEmpty(value) {
1763
1763
  function keptSlotsOf(count) {
1764
1764
  return Math.max(2, Math.floor(count / 4));
1765
1765
  }
1766
+ function reservedSlotOf(count) {
1767
+ return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
1768
+ }
1766
1769
  function gamesKeptOf(kept) {
1767
1770
  return Math.max(0, Math.min(Math.floor(kept / 2), kept - GATE_KEPT - 1));
1768
1771
  }
@@ -1777,7 +1780,8 @@ function slotsDrawnOn(count, taker, queue) {
1777
1780
  return { numbers: every };
1778
1781
  }
1779
1782
  if (taker === "tier") {
1780
- return { numbers: [...kept].reverse(), limit: "tier" };
1783
+ const reserve = reservedSlotOf(count);
1784
+ return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
1781
1785
  }
1782
1786
  return { numbers: kept.slice(0, Math.max(1, kept.length - GATE_KEPT)), limit: "kept" };
1783
1787
  }
@@ -1926,7 +1930,7 @@ class EngineSlots {
1926
1930
  try {
1927
1931
  for (;; ) {
1928
1932
  signal?.throwIfAborted();
1929
- const lock = await this.takeFree(game);
1933
+ const lock = await this.takeFree(game, Date.now() - began);
1930
1934
  if (lock !== null) {
1931
1935
  return lock;
1932
1936
  }
@@ -1958,7 +1962,7 @@ class EngineSlots {
1958
1962
  }
1959
1963
  }
1960
1964
  }
1961
- async takeFree(game = false) {
1965
+ async takeFree(game = false, waitedMs = 0) {
1962
1966
  mkdirSync2(this.directory, { recursive: true });
1963
1967
  await this.sweep();
1964
1968
  const inherited = this.inherited === null ? await this.inheritedLock() : null;
@@ -1967,11 +1971,14 @@ class EngineSlots {
1967
1971
  return inherited;
1968
1972
  }
1969
1973
  const taker = this.taker();
1970
- this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" ? "none" : await this.gateQueue());
1974
+ const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
1975
+ this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" || aged ? "none" : await this.gateQueue());
1971
1976
  if (game && this.drawn.limit === "kept") {
1972
- const share = slotsDrawnOn(this.count, "tier", "queued").numbers;
1973
- const games = await this.heldByGames(share);
1974
- if (games.length >= gamesKeptOf(share.length)) {
1977
+ const reserve = reservedSlotOf(this.count);
1978
+ this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
1979
+ const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
1980
+ const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
1981
+ if (games.length >= gamesKeptOf(kept)) {
1975
1982
  this.drawn = { numbers: games, limit: "games" };
1976
1983
  return null;
1977
1984
  }
@@ -2122,6 +2129,25 @@ class EngineSlots {
2122
2129
  await this.cleared(join5(this.directory, name), false);
2123
2130
  }
2124
2131
  }
2132
+ const marks = join5(this.directory, WAITING_FOLDER);
2133
+ let waits;
2134
+ try {
2135
+ waits = readdirSync(marks);
2136
+ } catch {
2137
+ return;
2138
+ }
2139
+ for (const name of waits) {
2140
+ let content;
2141
+ try {
2142
+ content = readWhole(join5(marks, name));
2143
+ } catch {
2144
+ continue;
2145
+ }
2146
+ const pid = content === null ? undefined : holderOf(content)?.pid;
2147
+ if (pid !== undefined && !alive(pid)) {
2148
+ removeWhole(join5(marks, name));
2149
+ }
2150
+ }
2125
2151
  }
2126
2152
  takenOver(lock, holder) {
2127
2153
  return this.cleared(lock, true, holder);
@@ -2344,7 +2370,7 @@ async function inASlot(body) {
2344
2370
  held.release();
2345
2371
  }
2346
2372
  }
2347
- var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2373
+ var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, ONE_OFF_AGED_MS = 120000, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2348
2374
  var init_engine_slots = __esm(() => {
2349
2375
  init_alive();
2350
2376
  init_cache_home();
@@ -17960,7 +17986,7 @@ var init_tool_definitions = __esm(() => {
17960
17986
  {
17961
17987
  name: "project_settings",
17962
17988
  title: "Project settings",
17963
- 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, and enable_plugin notes a script it would not load. 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, and overrides with overridesNote when a feature's override still replaces it. add_autoload refuses a name the editor's Autoload tab refuses: not an identifier, a keyword, a built-in type or an engine class. add_input_action on an existing action, a ui_ one included, replaces its events, and its deadzone when one is given, answering was_new false with 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 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, its note naming the nearest settings when the engine does not declare it either. 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 with no engine default, such as an input action the project added.",
17989
+ 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 names from the file (named under editorAdopted), even one the file already held, or reloads the bus layout, so it does not save the old values 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, and enable_plugin notes a script it would not load. 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, and overrides with overridesNote when a feature's override still replaces it. add_autoload refuses a name the editor's Autoload tab refuses: not an identifier, a keyword, a built-in type or an engine class. add_input_action on an existing action, a ui_ one included, replaces its events, and its deadzone when one is given, answering was_new false with 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 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, its note naming the nearest settings when the engine does not declare it either. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place. 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; it is absent for a setting with no engine default, such as an input action the project added. A setting res://override.cfg or the project_settings_override file sets, which a run lays over project.godot and the editor does not, is answered with the project's value and overridden_in, the file and the value a run reads; a set adds overriddenNote.",
17964
17990
  parameters: {
17965
17991
  projectPath: PROJECT_PATH,
17966
17992
  setting: {
@@ -18158,7 +18184,7 @@ var init_tool_definitions = __esm(() => {
18158
18184
  direction: {
18159
18185
  type: "string",
18160
18186
  enum: ["forward", "reverse"],
18161
- description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18187
+ description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. A file whose header, or an import sidecar, claims the same uid as its own is no use either: it is listed under shares_uid, since the engine maps a uid to one file and loads that one wherever the uid is named. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18162
18188
  },
18163
18189
  depth: {
18164
18190
  type: "integer",
@@ -19253,7 +19279,7 @@ var init_tool_definitions = __esm(() => {
19253
19279
  title: "Editor status",
19254
19280
  effect: "read",
19255
19281
  description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and editorCannotConnect, restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. A false editor.lspListening or editor.dapListening comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
19256
- answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved; all three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs headless with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
19282
+ answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves on this release's runtime addon, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved; all three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs headless with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
19257
19283
  parameters: {},
19258
19284
  requires: []
19259
19285
  },
@@ -55661,7 +55687,7 @@ ${error.text}`;
55661
55687
  run,
55662
55688
  playingInEditor: playing,
55663
55689
  ...hold,
55664
- runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false),
55690
+ runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false && game.addonIsStale !== true),
55665
55691
  runtimes: games,
55666
55692
  unreadable: announced.unspoken.map((game) => ({
55667
55693
  pid: game.pid,
@@ -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.
@@ -16,8 +16,9 @@ const SETTLE_MS: int = 8000
16
16
  ## How long a window with nothing in it is walked before the client stops: a game building its tree
17
17
  ## fills the window within a few frames of being asked, and one building none never does.
18
18
  const EMPTY_MS: int = 4000
19
- ## How long starting PowerShell and loading UI Automation may take before the client is stopped. It
20
- ## waits on the machine alone: a warm start takes under a second, and a cold one on a loaded Windows
19
+ ## How long starting PowerShell, loading UI Automation and the client's other cold loads may take
20
+ ## before the client is stopped. It waits on the machine alone: a warm start takes under a second,
21
+ ## and a cold one on a loaded Windows
21
22
  ## runner took the whole of a single 23 s budget meant for the walk as well.
22
23
  const START_MS: int = 45000
23
24
  ## How long the walks may take beyond [constant SETTLE_MS] a window once UI Automation is loaded,
@@ -39,6 +40,7 @@ const NODE_ID: String = "_node_instance"
39
40
  const WALK: String = r"""
40
41
  $ErrorActionPreference = 'Stop'
41
42
  function Answer($value) {
43
+ Mark 'answering'
42
44
  [System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
43
45
  Mark 'answered'
44
46
  }
@@ -48,7 +50,6 @@ function Mark($stage) {
48
50
  try {
49
51
  Mark 'began'
50
52
  Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
51
- Mark 'loaded'
52
53
  $walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
53
54
  function Patterned($element, $name) {
54
55
  $pattern = ("System.Windows.Automation.$($name)Pattern" -as [type])::Pattern
@@ -70,6 +71,10 @@ try {
70
71
  } catch {
71
72
  $script:unread = "$_"
72
73
  }
74
+ # Serialised once here, since the first ConvertTo-Json loads its serialiser, and on a loaded
75
+ # machine that cold load belongs to the start's budget, not to the walks'.
76
+ $null = ConvertTo-Json -InputObject @{ warm = @(1) } -Depth 100 -Compress
77
+ Mark 'loaded'
73
78
  function FullDescription($element) {
74
79
  if ($null -ne $script:unread) { return '' }
75
80
  try {
@@ -281,7 +286,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
281
286
  var looked_at: int = began_at
282
287
  while OS.is_process_running(pid):
283
288
  var now: int = Time.get_ticks_msec()
284
- if now - looked_at >= LOOK_MS:
289
+ # Read again at the deadline too, so an answer marked since the last look is taken rather
290
+ # than stopped.
291
+ if now - looked_at >= LOOK_MS or now > deadline:
285
292
  looked_at = now
286
293
  var marked: String = FileAccess.get_file_as_string(progress)
287
294
  if not walking and marked.contains("loaded "):
@@ -756,8 +763,8 @@ static func _marks(progress: String) -> Array:
756
763
 
757
764
  ## How far a client started at [param started_ms] and stopped at [param stopped_ms] got by the marks
758
765
  ## in [param progress], said for a caller whose read it stopped: the time went to starting
759
- ## PowerShell, loading UI Automation, the walks, or after the last walk, and which tells a loaded
760
- ## machine from a window that never answers.
766
+ ## PowerShell, loading UI Automation, the walks, after the last walk, or writing the answer, and
767
+ ## which tells a loaded machine from a window that never answers.
761
768
  static func _reached(progress: String, started_ms: int, stopped_ms: int) -> String:
762
769
  var marks: Array = _marks(progress)
763
770
  var at: Dictionary = {}
@@ -786,7 +793,7 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
786
793
  )
787
794
  % [began, loaded]
788
795
  )
789
- return (
796
+ var walked: String = (
790
797
  (
791
798
  "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
792
799
  + "windows were walked %d time%s, the last finding %s element%s and ending %d ms "
@@ -802,6 +809,10 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
802
809
  stopped_ms - last_at,
803
810
  ]
804
811
  )
812
+ if not at.has("answering"):
813
+ return walked
814
+ var answering: int = stopped_ms - at["answering"]
815
+ return walked + ", which had been writing the answer for %d ms" % answering
805
816
 
806
817
 
807
818
  ## Where the time of a client started at [param started_ms] and done at [param done_ms] went, by
@@ -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():
@@ -281,6 +281,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
281
281
  var by_kind: Dictionary = {}
282
282
  var total: int = 0
283
283
  var in_code: int = 0
284
+ var shares_uid: Array[String] = []
285
+ var target_uid: String = target["uid"]
284
286
 
285
287
  for file_path: String in all_files:
286
288
  # Its own sidecar names it as the file it was imported from, which is not a use of it.
@@ -305,6 +307,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
305
307
  var references: Array[Dictionary] = []
306
308
  for i: int in range(lines.size()):
307
309
  var line: String = lines[i]
310
+ # A file declaring the target's uid as its own is not using it: a resource copied with
311
+ # its header claims the same uid, and its header line was answered as a use by path.
312
+ if not target_uid.is_empty() and _own_uid(line, i, file_path) == target_uid:
313
+ shares_uid.append(file_path)
314
+ continue
308
315
  var by_its_path: bool = _names_target(line, file_path, target)
309
316
  var by_its_name: bool = false
310
317
  var name_in_comment: bool = false
@@ -408,9 +415,30 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
408
415
  }
409
416
  if not autoload_named.is_empty():
410
417
  answer["autoload"] = autoload_named
418
+ if not shares_uid.is_empty():
419
+ answer["shares_uid"] = shares_uid
411
420
  return answer
412
421
 
413
422
 
423
+ # The uid [param line], line [param index] of [param file_path], declares as the file's own: a
424
+ # scene's or a resource's header, or an import sidecar's uid of the file it imports; empty for any
425
+ # other line.
426
+ static func _own_uid(line: String, index: int, file_path: String) -> String:
427
+ var trimmed: String = line.strip_edges()
428
+ var declares: bool = (
429
+ (
430
+ index == 0
431
+ and file_path.get_extension() in ["tscn", "tres"]
432
+ and (trimmed.begins_with("[gd_scene") or trimmed.begins_with("[gd_resource"))
433
+ )
434
+ or (file_path.get_extension() == "import" and trimmed.begins_with('uid="'))
435
+ )
436
+ if not declares:
437
+ return ""
438
+ var found: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(trimmed)
439
+ return "" if found == null else found.get_string(1)
440
+
441
+
414
442
  # The class_name a script declares, or empty for a scene, a resource or a script without one.
415
443
  func _declared_class_name(path: String) -> String:
416
444
  if not path.ends_with(".gd"):
@@ -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,
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.57",
3
+ "version": "1.3.59",
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",