gdharness 1.3.8 → 1.3.9
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 +137 -66
- package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
- package/build/godot/addons/gdharness_editor/reading.gd +61 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
- package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +3 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +27 -31
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +11 -5
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
- package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
- package/build/godot/operations/editor_tools.gd +8 -0
- package/build/godot/operations/file_reads.gd +13 -0
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +1 -1
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/input_actions.gd +22 -7
- package/build/godot/operations/project_config.gd +31 -0
- package/build/godot/operations/reading.gd +61 -5
- package/build/godot/operations/serialisation.gd +8 -7
- package/build/keeper.js +43 -13
- package/package.json +1 -1
|
@@ -166,6 +166,37 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
166
166
|
return answer
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
# Takes a setting's line out of project.godot, so the engine's default holds again, or nothing at all
|
|
170
|
+
# for one the engine does not declare: the way back from a set, which writes a misspelt name as given.
|
|
171
|
+
func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
172
|
+
var setting_path: String = str(params.get("setting", ""))
|
|
173
|
+
if setting_path.is_empty():
|
|
174
|
+
return _log.failure("setting is required")
|
|
175
|
+
var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
|
|
176
|
+
var held: bool = ProjectSettings.has_setting(setting_path)
|
|
177
|
+
var value: Variant = ProjectSettings.get_setting(setting_path) if held else null
|
|
178
|
+
var answer: Dictionary = {
|
|
179
|
+
"setting_path": setting_path,
|
|
180
|
+
"default_value": _values.serialize_value(default_value) if default_value != null else null,
|
|
181
|
+
}
|
|
182
|
+
# A setting at the engine's default has no line to take out, as the engine leaves it out of the file.
|
|
183
|
+
if not held or (default_value != null and value == default_value):
|
|
184
|
+
answer["removed"] = false
|
|
185
|
+
answer["saved"] = false
|
|
186
|
+
answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
|
|
187
|
+
return answer
|
|
188
|
+
answer["old_value"] = _values.serialize_value(value)
|
|
189
|
+
# The default rather than null for a setting the engine declares: null takes the declaration away
|
|
190
|
+
# as well, and a value at its default is left out of the file all the same.
|
|
191
|
+
ProjectSettings.set_setting(setting_path, default_value)
|
|
192
|
+
var err: Error = GdhProjectFile.save()
|
|
193
|
+
if err != OK:
|
|
194
|
+
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
195
|
+
answer["removed"] = true
|
|
196
|
+
answer["saved"] = true
|
|
197
|
+
return answer
|
|
198
|
+
|
|
199
|
+
|
|
169
200
|
# The settings in the same section as [param setting_path] named nearest it, for a new one that is
|
|
170
201
|
# likely a misspelt one: a setting nothing declares is written all the same, and is never read.
|
|
171
202
|
static func _settings_near(setting_path: String) -> PackedStringArray:
|
|
@@ -24,6 +24,23 @@ extends RefCounted
|
|
|
24
24
|
|
|
25
25
|
## How many of a node's children a refusal for a path missing below it names.
|
|
26
26
|
const NAMED_CHILDREN: int = 12
|
|
27
|
+
## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
|
|
28
|
+
const SHAPES_OF_REALS: Array[int] = [
|
|
29
|
+
TYPE_VECTOR2,
|
|
30
|
+
TYPE_VECTOR3,
|
|
31
|
+
TYPE_VECTOR4,
|
|
32
|
+
TYPE_RECT2,
|
|
33
|
+
TYPE_PLANE,
|
|
34
|
+
TYPE_QUATERNION,
|
|
35
|
+
TYPE_AABB,
|
|
36
|
+
TYPE_BASIS,
|
|
37
|
+
TYPE_TRANSFORM2D,
|
|
38
|
+
TYPE_TRANSFORM3D,
|
|
39
|
+
TYPE_PROJECTION,
|
|
40
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
41
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
42
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
43
|
+
]
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
@@ -190,18 +207,26 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
190
207
|
|
|
191
208
|
|
|
192
209
|
## [param said], the refusal for [param property], which [param target] does not declare, with the
|
|
193
|
-
## names it declares nearest it when any are, as a misspelt one is usually one of them
|
|
194
|
-
##
|
|
210
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
|
|
211
|
+
## variables its script declares. The engine's own are not listed when none is near, since an object
|
|
212
|
+
## has a hundred, while a script's are the few a caller most likely meant.
|
|
195
213
|
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
214
|
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
215
|
var names: Array[String] = []
|
|
216
|
+
var scripted: Array[String] = []
|
|
198
217
|
for declared: Dictionary in target.get_property_list():
|
|
199
|
-
|
|
218
|
+
var usage: int = as_int(declared.get("usage", 0))
|
|
219
|
+
if not usage & headings:
|
|
200
220
|
names.append(str(declared.get("name", "")))
|
|
221
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
|
|
222
|
+
scripted.append(str(declared.get("name", "")))
|
|
201
223
|
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
-
if close.is_empty():
|
|
224
|
+
if not close.is_empty():
|
|
225
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
226
|
+
if scripted.is_empty():
|
|
203
227
|
return said
|
|
204
|
-
|
|
228
|
+
scripted.sort()
|
|
229
|
+
return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
|
|
205
230
|
|
|
206
231
|
|
|
207
232
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
@@ -219,3 +244,34 @@ static func signals_of(object: Object) -> Array:
|
|
|
219
244
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
220
245
|
static func with_article(word: String) -> String:
|
|
221
246
|
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
|
|
250
|
+
## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
|
|
251
|
+
## the same number, but not the one written or the one the file shows, so a caller comparing what it
|
|
252
|
+
## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
|
|
253
|
+
## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
|
|
254
|
+
## 32 bits or 64.
|
|
255
|
+
static func single(value: float) -> float:
|
|
256
|
+
if not is_finite(value) or value == 0.0:
|
|
257
|
+
return value
|
|
258
|
+
var held: float = PackedFloat32Array([value])[0]
|
|
259
|
+
var magnitude: int = floori(log(absf(held)) / log(10.0))
|
|
260
|
+
for digits: int in range(1, 10):
|
|
261
|
+
var shift: int = digits - 1 - magnitude
|
|
262
|
+
var tried: float = (
|
|
263
|
+
roundf(held * pow(10.0, shift)) / pow(10.0, shift)
|
|
264
|
+
if shift >= 0
|
|
265
|
+
else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
|
|
266
|
+
)
|
|
267
|
+
if PackedFloat32Array([tried])[0] == held:
|
|
268
|
+
return tried
|
|
269
|
+
return held
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
|
|
273
|
+
## the shapes built of them unless the engine was built for 64.
|
|
274
|
+
static func holds_singles(type: int) -> bool:
|
|
275
|
+
if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
|
|
276
|
+
return true
|
|
277
|
+
return not OS.has_feature("double") and type in SHAPES_OF_REALS
|
|
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
|
|
|
142
142
|
return value
|
|
143
143
|
var carried: Variant = call(serialiser, value)
|
|
144
144
|
# The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
|
|
145
|
-
# tagged here; containers reach their members through this
|
|
146
|
-
|
|
145
|
+
# tagged here, and a 32-bit one shortened; containers reach their members through this already.
|
|
146
|
+
var type: int = typeof(value)
|
|
147
|
+
return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
# [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
|
|
150
151
|
# NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
|
|
151
152
|
# back out as null, so a property holding INF was answered as holding nothing.
|
|
152
|
-
func _finite(carried: Variant) -> Variant:
|
|
153
|
+
func _finite(carried: Variant, single: bool = false) -> Variant:
|
|
153
154
|
if carried is float:
|
|
154
155
|
var number: float = carried
|
|
155
|
-
return _serialize_float(number)
|
|
156
|
+
return _serialize_float(GdhRead.single(number) if single else number)
|
|
156
157
|
if carried is Dictionary:
|
|
157
158
|
var fields: Dictionary = carried
|
|
158
159
|
var tagged: Dictionary = {}
|
|
159
160
|
for key: Variant in fields:
|
|
160
|
-
tagged[key] = _finite(fields[key])
|
|
161
|
+
tagged[key] = _finite(fields[key], single)
|
|
161
162
|
return tagged
|
|
162
163
|
if carried is Array:
|
|
163
164
|
var items: Array = carried
|
|
164
165
|
var tagged_items: Array = []
|
|
165
166
|
for item: Variant in items:
|
|
166
|
-
tagged_items.append(_finite(item))
|
|
167
|
+
tagged_items.append(_finite(item, single))
|
|
167
168
|
return tagged_items
|
|
168
169
|
return carried
|
|
169
170
|
|
|
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
|
|
|
189
190
|
for each: float in numbers:
|
|
190
191
|
items.append(_serialize_float(each))
|
|
191
192
|
return items
|
|
192
|
-
return value
|
|
193
|
+
return numbers if value is PackedFloat32Array else value
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
# Rebuilds a Godot value from the shape serialize_value gave it.
|
package/build/keeper.js
CHANGED
|
@@ -814,11 +814,13 @@ class GameDebugger {
|
|
|
814
814
|
server;
|
|
815
815
|
port;
|
|
816
816
|
breakpoints;
|
|
817
|
+
breakpointLines;
|
|
817
818
|
observers;
|
|
818
819
|
constructor(server, port, breakpoints, observers) {
|
|
819
820
|
this.server = server;
|
|
820
821
|
this.port = port;
|
|
821
822
|
this.breakpoints = breakpoints;
|
|
823
|
+
this.breakpointLines = new Set(breakpoints);
|
|
822
824
|
this.observers = observers;
|
|
823
825
|
}
|
|
824
826
|
static async open(breakpoints, observers = []) {
|
|
@@ -1010,6 +1012,11 @@ class GameDebugger {
|
|
|
1010
1012
|
}
|
|
1011
1013
|
switch (request.op) {
|
|
1012
1014
|
case "breakpoint":
|
|
1015
|
+
if (request.set) {
|
|
1016
|
+
this.breakpointLines.add(`${request.script}:${String(request.line)}`);
|
|
1017
|
+
} else {
|
|
1018
|
+
this.breakpointLines.delete(`${request.script}:${String(request.line)}`);
|
|
1019
|
+
}
|
|
1013
1020
|
this.send("breakpoint", [request.script, request.line, request.set], this.held?.thread ?? MAIN_THREAD);
|
|
1014
1021
|
return { sent: true };
|
|
1015
1022
|
case "pause":
|
|
@@ -1087,18 +1094,12 @@ class GameDebugger {
|
|
|
1087
1094
|
};
|
|
1088
1095
|
}
|
|
1089
1096
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
return {
|
|
1097
|
-
paused: false,
|
|
1098
|
-
note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
frames = await this.stack();
|
|
1097
|
+
const frames = await this.pastTheAddon();
|
|
1098
|
+
if (frames === "lost") {
|
|
1099
|
+
return {
|
|
1100
|
+
paused: false,
|
|
1101
|
+
note: `The game stopped in gdharness's runtime addon and was stepped on to reach its own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script it runs; debug_state stack reads it then.`
|
|
1102
|
+
};
|
|
1102
1103
|
}
|
|
1103
1104
|
this.stoppedBy("Paused");
|
|
1104
1105
|
const stack = frames ?? [];
|
|
@@ -1111,6 +1112,19 @@ class GameDebugger {
|
|
|
1111
1112
|
} : {}
|
|
1112
1113
|
};
|
|
1113
1114
|
}
|
|
1115
|
+
async pastTheAddon() {
|
|
1116
|
+
let frames = await this.stack();
|
|
1117
|
+
const stepping = Date.now();
|
|
1118
|
+
while (frames !== null && this.held !== null && (frames[0]?.script.startsWith(RUNTIME_ADDON) ?? false) && Date.now() - stepping < ADDON_STEP_MS) {
|
|
1119
|
+
this.forget("debug_enter");
|
|
1120
|
+
this.send("step", [], this.held.thread);
|
|
1121
|
+
if (await this.next(GameDebugger.named("debug_enter"), STEP_MS) === null) {
|
|
1122
|
+
return "lost";
|
|
1123
|
+
}
|
|
1124
|
+
frames = await this.stack();
|
|
1125
|
+
}
|
|
1126
|
+
return frames;
|
|
1127
|
+
}
|
|
1114
1128
|
stoppedBy(cause) {
|
|
1115
1129
|
if (this.held !== null) {
|
|
1116
1130
|
this.held = { ...this.held, reason: cause };
|
|
@@ -1139,8 +1153,24 @@ class GameDebugger {
|
|
|
1139
1153
|
note: `The game had not stopped again ${STEP_MS / 1000}s after the step: the step is still running, as one out of a function that never returns does, or it went on without reaching another line. debug_state stack answers once it stops, and debug_control pause stops it wherever it is.`
|
|
1140
1154
|
};
|
|
1141
1155
|
}
|
|
1156
|
+
const frames = await this.pastTheAddon();
|
|
1157
|
+
if (frames === "lost") {
|
|
1158
|
+
return {
|
|
1159
|
+
stepped: true,
|
|
1160
|
+
heldAgain: false,
|
|
1161
|
+
note: `The step stopped in gdharness's runtime addon and was stepped on to reach the game's own script, and did not stop again within ${STEP_MS / 1000}s. It stops at the next line of script the game runs; debug_state stack reads it then.`
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1142
1164
|
this.stoppedBy("Stepped");
|
|
1143
|
-
|
|
1165
|
+
const stack = frames ?? [];
|
|
1166
|
+
const top = stack[0];
|
|
1167
|
+
const onBreakpoint = top !== undefined && this.breakpointLines.has(`${top.script}:${String(top.line)}`);
|
|
1168
|
+
return {
|
|
1169
|
+
stepped: true,
|
|
1170
|
+
heldAgain: true,
|
|
1171
|
+
held: { ...this.heldState(), ...onBreakpoint ? { onBreakpoint: true } : {} },
|
|
1172
|
+
stack
|
|
1173
|
+
};
|
|
1144
1174
|
}
|
|
1145
1175
|
async variables(frame) {
|
|
1146
1176
|
const held = this.held;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
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",
|