gdharness 1.3.3 → 1.3.5
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 +532 -456
- package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
- package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +3 -2
- package/build/godot/operations/classdb_queries.gd +112 -46
- package/build/godot/operations/declared_values.gd +181 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +4 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +157 -36
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +192 -231
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/build/keeper.js +83 -69
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## Whether a value read back is the value that was given, as the tools that write properties judge
|
|
5
|
+
## what a write kept and what it changed besides.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
const GdhSerialisation = preload("serialisation.gd")
|
|
9
|
+
|
|
10
|
+
# The types made of floats, which a read back compares within float precision: a Vector2 property
|
|
11
|
+
# stores 32-bit components, so 0.1 comes back as 0.100000001.
|
|
12
|
+
const FLOAT_SHAPES: Array[int] = [
|
|
13
|
+
TYPE_VECTOR2,
|
|
14
|
+
TYPE_VECTOR3,
|
|
15
|
+
TYPE_VECTOR4,
|
|
16
|
+
TYPE_QUATERNION,
|
|
17
|
+
TYPE_COLOR,
|
|
18
|
+
TYPE_RECT2,
|
|
19
|
+
TYPE_TRANSFORM2D,
|
|
20
|
+
TYPE_BASIS,
|
|
21
|
+
TYPE_TRANSFORM3D,
|
|
22
|
+
TYPE_PLANE,
|
|
23
|
+
TYPE_AABB,
|
|
24
|
+
TYPE_PROJECTION,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const PACKED: Array[int] = [
|
|
28
|
+
TYPE_PACKED_BYTE_ARRAY,
|
|
29
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
30
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
31
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
32
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
33
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
34
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
35
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
36
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
37
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# The pairs of built-in resources being compared, so a resource that holds itself ends the
|
|
41
|
+
# comparison instead of recursing into it for ever.
|
|
42
|
+
static var _comparing: Array[Array] = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Whether [param held] is [param given], as a property read back after a set answers it.
|
|
46
|
+
##
|
|
47
|
+
## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
|
|
48
|
+
## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
|
|
49
|
+
## given compares element by element, since `==` between the two is an error rather than false.
|
|
50
|
+
static func same(held: Variant, given: Variant) -> bool:
|
|
51
|
+
var held_type: int = typeof(held)
|
|
52
|
+
var given_type: int = typeof(given)
|
|
53
|
+
# An empty object property reads back as an Object that is null, not as nil, and the two are
|
|
54
|
+
# the same nothing.
|
|
55
|
+
var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
|
|
56
|
+
var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
|
|
57
|
+
if held_none or given_none:
|
|
58
|
+
return held_none == given_none
|
|
59
|
+
if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
|
|
60
|
+
return _same_object(held, given)
|
|
61
|
+
if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
|
|
62
|
+
if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
|
|
63
|
+
return held_type == given_type and held == given
|
|
64
|
+
var held_number: float = GdhRead.as_float(held)
|
|
65
|
+
var given_number: float = GdhRead.as_float(given)
|
|
66
|
+
return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
|
|
67
|
+
if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
|
|
68
|
+
return str(held) == str(given)
|
|
69
|
+
if _listed(held_type) and _listed(given_type):
|
|
70
|
+
var held_items: Array = type_convert(held, TYPE_ARRAY)
|
|
71
|
+
var given_items: Array = type_convert(given, TYPE_ARRAY)
|
|
72
|
+
if held_items.size() != given_items.size():
|
|
73
|
+
return false
|
|
74
|
+
for index: int in held_items.size():
|
|
75
|
+
if not same(held_items[index], given_items[index]):
|
|
76
|
+
return false
|
|
77
|
+
return true
|
|
78
|
+
if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
|
|
79
|
+
var held_entries: Dictionary = held
|
|
80
|
+
var given_entries: Dictionary = given
|
|
81
|
+
return _same_entries(held_entries, given_entries)
|
|
82
|
+
if held_type != given_type:
|
|
83
|
+
return false
|
|
84
|
+
return _same_shape(held, given)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
static func _same_object(held: Variant, given: Variant) -> bool:
|
|
88
|
+
if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
|
|
89
|
+
return false
|
|
90
|
+
if is_same(held, given):
|
|
91
|
+
return true
|
|
92
|
+
if not held is Resource or not given is Resource:
|
|
93
|
+
return false
|
|
94
|
+
var held_resource: Resource = held
|
|
95
|
+
var given_resource: Resource = given
|
|
96
|
+
if not _built_in(held_resource):
|
|
97
|
+
return held_resource.resource_path == given_resource.resource_path
|
|
98
|
+
if held_resource.get_class() != given_resource.get_class():
|
|
99
|
+
return false
|
|
100
|
+
# A resource local to the scene is duplicated for every instance, so the copy read back is
|
|
101
|
+
# another object of the same class.
|
|
102
|
+
if held_resource.resource_local_to_scene:
|
|
103
|
+
return true
|
|
104
|
+
return _built_in(given_resource) and _same_stored(held_resource, given_resource)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## A resource no other file can refer to: one with no path, or one of a file's own sub-resources,
|
|
108
|
+
## whose path is the file's with "::" and an id after it.
|
|
109
|
+
static func _built_in(resource: Resource) -> bool:
|
|
110
|
+
return resource.resource_path.is_empty() or resource.resource_path.contains("::")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## Two built-in resources of one class, by every property they store. Neither has a path to be
|
|
114
|
+
## known by, and a file loaded again holds a new object for each, so a Curve written into a
|
|
115
|
+
## resource file and read back was judged lost by the save that had just kept it.
|
|
116
|
+
static func _same_stored(held: Resource, given: Resource) -> bool:
|
|
117
|
+
for pair: Array in _comparing:
|
|
118
|
+
if is_same(pair[0], held) and is_same(pair[1], given):
|
|
119
|
+
return true
|
|
120
|
+
_comparing.append([held, given])
|
|
121
|
+
var alike: bool = true
|
|
122
|
+
for property: Dictionary in held.get_property_list():
|
|
123
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
124
|
+
var name: String = str(property.get("name", ""))
|
|
125
|
+
if (
|
|
126
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
127
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
128
|
+
):
|
|
129
|
+
continue
|
|
130
|
+
if not same(held.get(name), given.get(name)):
|
|
131
|
+
alike = false
|
|
132
|
+
break
|
|
133
|
+
_comparing.pop_back()
|
|
134
|
+
return alike
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
|
|
138
|
+
if held.size() != given.size():
|
|
139
|
+
return false
|
|
140
|
+
for key: Variant in given:
|
|
141
|
+
var found: bool = false
|
|
142
|
+
for held_key: Variant in held:
|
|
143
|
+
if same(held_key, key):
|
|
144
|
+
found = same(held[held_key], given[key])
|
|
145
|
+
break
|
|
146
|
+
if not found:
|
|
147
|
+
return false
|
|
148
|
+
return true
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Two values of one type made of floats, compared component by component through the type's own
|
|
152
|
+
## text form, which lists every component: each type has its own is_equal_approx, and reaching it
|
|
153
|
+
## through a Variant is a call the typed gate refuses.
|
|
154
|
+
static func _same_shape(held: Variant, given: Variant) -> bool:
|
|
155
|
+
if typeof(held) not in FLOAT_SHAPES:
|
|
156
|
+
return held == given
|
|
157
|
+
var held_parts: Array[float] = _components(held)
|
|
158
|
+
var given_parts: Array[float] = _components(given)
|
|
159
|
+
if held_parts.size() != given_parts.size():
|
|
160
|
+
return false
|
|
161
|
+
for index: int in held_parts.size():
|
|
162
|
+
if not same(held_parts[index], given_parts[index]):
|
|
163
|
+
return false
|
|
164
|
+
return true
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
static func _components(value: Variant) -> Array[float]:
|
|
168
|
+
var parts: Array[float] = []
|
|
169
|
+
var text: String = var_to_str(value)
|
|
170
|
+
var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
|
|
171
|
+
for part: String in inner.split(",", false):
|
|
172
|
+
parts.append(part.strip_edges().to_float())
|
|
173
|
+
return parts
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
static func _listed(type: int) -> bool:
|
|
177
|
+
return type == TYPE_ARRAY or PACKED.has(type)
|
|
@@ -121,6 +121,10 @@ const BUILDERS: Dictionary = {
|
|
|
121
121
|
"Dictionary": "_build_dictionary",
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
## The built-in resources being described, outermost first, so one holding itself somewhere inside
|
|
125
|
+
## is answered by its class rather than without end.
|
|
126
|
+
var _describing: Array[Resource] = []
|
|
127
|
+
|
|
124
128
|
|
|
125
129
|
# Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
126
130
|
# passes through as itself, which is what the JSON-native ones want.
|
|
@@ -665,9 +669,21 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
665
669
|
return _describe_input_event(event)
|
|
666
670
|
if value is Resource:
|
|
667
671
|
var resource: Resource = value
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
# A file's built-in resources load with a path of the file's own and "::", which no other
|
|
673
|
+
# file can refer to.
|
|
674
|
+
if resource.resource_path.is_empty() or resource.resource_path.contains("::"):
|
|
675
|
+
return _describe_built_in(resource)
|
|
676
|
+
var filed: Dictionary = {
|
|
677
|
+
"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"
|
|
678
|
+
}
|
|
679
|
+
# The engine class of a resource with a script is the one its script extends, so a Stats
|
|
680
|
+
# file read as a Resource and nothing said it was a Stats.
|
|
681
|
+
var attached: Variant = resource.get_script()
|
|
682
|
+
if attached is Script:
|
|
683
|
+
var script: Script = attached
|
|
684
|
+
if not script.get_global_name().is_empty():
|
|
685
|
+
filed["scriptClass"] = str(script.get_global_name())
|
|
686
|
+
return filed
|
|
671
687
|
if value is Node:
|
|
672
688
|
var node: Node = value
|
|
673
689
|
if node.is_inside_tree():
|
|
@@ -675,6 +691,62 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
675
691
|
return _describe_object(value)
|
|
676
692
|
|
|
677
693
|
|
|
694
|
+
## A resource built into the file it was read from, in the form a write builds one from: its class
|
|
695
|
+
## under _type beside every property it stores away from its default. As its class alone it said
|
|
696
|
+
## nothing of what it held, so a CollisionShape2D's radius could not be read, and it could not be
|
|
697
|
+
## written back.
|
|
698
|
+
func _describe_built_in(resource: Resource) -> Dictionary:
|
|
699
|
+
if _describing.has(resource):
|
|
700
|
+
return {"class": resource.get_class(), "_type": "Resource"}
|
|
701
|
+
_describing.append(resource)
|
|
702
|
+
var described: Dictionary = {"_type": resource.get_class()}
|
|
703
|
+
var attached: Variant = resource.get_script()
|
|
704
|
+
var script: Script = null
|
|
705
|
+
if attached is Script:
|
|
706
|
+
script = attached
|
|
707
|
+
described["script"] = serialize_value(script)
|
|
708
|
+
var fresh: Object = _fresh(script)
|
|
709
|
+
for property: Dictionary in resource.get_property_list():
|
|
710
|
+
var usage: int = property.get("usage", 0)
|
|
711
|
+
var name: String = str(property.get("name", ""))
|
|
712
|
+
if (
|
|
713
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
714
|
+
or name == "script"
|
|
715
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
var held: Variant = resource.get(name)
|
|
719
|
+
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
720
|
+
if fresh != null:
|
|
721
|
+
default = fresh.get(name)
|
|
722
|
+
elif script != null and usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
723
|
+
default = script.get_property_default_value(name)
|
|
724
|
+
if typeof(held) == typeof(default) and held == default:
|
|
725
|
+
continue
|
|
726
|
+
described[name] = serialize_value(held)
|
|
727
|
+
_describing.pop_back()
|
|
728
|
+
return described
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
## A new instance of [param script] to read defaults off, or null where none should be made. What
|
|
732
|
+
## the script's own code sets as it is made (a resource_name in _init, a list built by a
|
|
733
|
+
## constructor, which the engine reports no default for) is then a default rather than a value
|
|
734
|
+
## the file holds. None is made for a script whose _init needs arguments, nor in the editor for one
|
|
735
|
+
## that is not a tool: the editor never runs those, and can_instantiate says so.
|
|
736
|
+
static func _fresh(script: Script) -> Object:
|
|
737
|
+
if script == null or not script.can_instantiate():
|
|
738
|
+
return null
|
|
739
|
+
for method: Dictionary in script.get_script_method_list():
|
|
740
|
+
if str(method.get("name", "")) == "_init":
|
|
741
|
+
var arguments: Array = method.get("args", [])
|
|
742
|
+
var defaults: Array = method.get("default_args", [])
|
|
743
|
+
if arguments.size() > defaults.size():
|
|
744
|
+
return null
|
|
745
|
+
# Called by name: new() is declared on each kind of script rather than on Script.
|
|
746
|
+
var made: Variant = script.call("new")
|
|
747
|
+
return made if made is Object else null
|
|
748
|
+
|
|
749
|
+
|
|
678
750
|
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
679
751
|
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
680
752
|
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|
package/build/keeper.js
CHANGED
|
@@ -3,9 +3,33 @@
|
|
|
3
3
|
// src/keeper.ts
|
|
4
4
|
import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync3, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
5
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join7 } from "node:path";
|
|
7
7
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8
8
|
|
|
9
|
+
// src/alive.ts
|
|
10
|
+
import process2 from "node:process";
|
|
11
|
+
function exitOf(child) {
|
|
12
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
13
|
+
return Promise.resolve({ code: child.exitCode, signal: child.signalCode });
|
|
14
|
+
}
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
child.once("exit", (code, signal) => {
|
|
17
|
+
resolve({ code, signal });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function alive(pid) {
|
|
22
|
+
if (pid === undefined || pid === null) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
process2.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return error.code === "EPERM";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
// src/desktop.ts
|
|
10
34
|
import { createHash } from "node:crypto";
|
|
11
35
|
import {
|
|
@@ -23,20 +47,6 @@ import { tmpdir } from "node:os";
|
|
|
23
47
|
import { join } from "node:path";
|
|
24
48
|
import { setTimeout as delay } from "node:timers/promises";
|
|
25
49
|
|
|
26
|
-
// src/alive.ts
|
|
27
|
-
import process2 from "node:process";
|
|
28
|
-
function alive(pid) {
|
|
29
|
-
if (pid === undefined || pid === null) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
process2.kill(pid, 0);
|
|
34
|
-
return true;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
return error.code === "EPERM";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
50
|
// src/spawn.ts
|
|
41
51
|
import {
|
|
42
52
|
execFile,
|
|
@@ -1031,7 +1041,7 @@ class GameDebugger {
|
|
|
1031
1041
|
}
|
|
1032
1042
|
if (!held.hasStack) {
|
|
1033
1043
|
this.forget("debug_enter");
|
|
1034
|
-
this.send("
|
|
1044
|
+
this.send("step", [], held.thread);
|
|
1035
1045
|
const again = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
1036
1046
|
if (again === null) {
|
|
1037
1047
|
return {
|
|
@@ -1253,8 +1263,28 @@ function readLaunched(printed) {
|
|
|
1253
1263
|
|
|
1254
1264
|
// src/run-record.ts
|
|
1255
1265
|
import { createHash as createHash2 } from "node:crypto";
|
|
1256
|
-
import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2,
|
|
1257
|
-
import { basename
|
|
1266
|
+
import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
|
|
1267
|
+
import { basename as basename2, join as join5, resolve as resolve3 } from "node:path";
|
|
1268
|
+
|
|
1269
|
+
// src/paths.ts
|
|
1270
|
+
import { realpathSync, statSync as statSync2 } from "node:fs";
|
|
1271
|
+
import { basename, dirname, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
1272
|
+
function realAsFarAsItExists(path) {
|
|
1273
|
+
const missing = [];
|
|
1274
|
+
let at = path;
|
|
1275
|
+
for (;; ) {
|
|
1276
|
+
try {
|
|
1277
|
+
return join3(realpathSync.native(at), ...missing);
|
|
1278
|
+
} catch {
|
|
1279
|
+
const parent = dirname(at);
|
|
1280
|
+
if (parent === at) {
|
|
1281
|
+
return path;
|
|
1282
|
+
}
|
|
1283
|
+
missing.unshift(basename(at));
|
|
1284
|
+
at = parent;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1258
1288
|
|
|
1259
1289
|
// src/windows-ask.ts
|
|
1260
1290
|
var SCRIPT = [
|
|
@@ -1278,7 +1308,7 @@ var SCRIPT = [
|
|
|
1278
1308
|
|
|
1279
1309
|
// src/runtime-client.ts
|
|
1280
1310
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
1281
|
-
import { dirname, join as
|
|
1311
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
|
|
1282
1312
|
|
|
1283
1313
|
// src/launch.ts
|
|
1284
1314
|
function envValue(name, variables = process.env) {
|
|
@@ -1323,7 +1353,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
1323
1353
|
return explicit;
|
|
1324
1354
|
}
|
|
1325
1355
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
1326
|
-
return
|
|
1356
|
+
return join4(perUser ?? tmpdir3(), "gdharness");
|
|
1327
1357
|
}
|
|
1328
1358
|
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
1329
1359
|
var verdicts = new Map;
|
|
@@ -1363,31 +1393,15 @@ function writeWhole(path, text) {
|
|
|
1363
1393
|
|
|
1364
1394
|
// src/run-record.ts
|
|
1365
1395
|
function runsDirectory() {
|
|
1366
|
-
return
|
|
1367
|
-
}
|
|
1368
|
-
function realAsFarAsItExists(path) {
|
|
1369
|
-
const missing = [];
|
|
1370
|
-
let at = path;
|
|
1371
|
-
for (;; ) {
|
|
1372
|
-
try {
|
|
1373
|
-
return join4(realpathSync.native(at), ...missing);
|
|
1374
|
-
} catch {
|
|
1375
|
-
const parent = dirname2(at);
|
|
1376
|
-
if (parent === at) {
|
|
1377
|
-
return path;
|
|
1378
|
-
}
|
|
1379
|
-
missing.unshift(basename(at));
|
|
1380
|
-
at = parent;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1396
|
+
return join5(runtimeDirectory(), "runs");
|
|
1383
1397
|
}
|
|
1384
1398
|
function projectKey(projectPath) {
|
|
1385
|
-
const real = realAsFarAsItExists(
|
|
1399
|
+
const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
|
|
1386
1400
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
1387
1401
|
return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
1388
1402
|
}
|
|
1389
1403
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
1390
|
-
return
|
|
1404
|
+
return join5(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
1391
1405
|
}
|
|
1392
1406
|
function writeRunRecord(record) {
|
|
1393
1407
|
mkdirSync(runsDirectory(), { recursive: true });
|
|
@@ -1454,14 +1468,14 @@ function recordAt(path) {
|
|
|
1454
1468
|
// src/scratch.ts
|
|
1455
1469
|
import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
|
|
1456
1470
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
1457
|
-
import { join as
|
|
1471
|
+
import { join as join6 } from "node:path";
|
|
1458
1472
|
import process4 from "node:process";
|
|
1459
1473
|
var SWEPT = { recursive: true, force: true };
|
|
1460
1474
|
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
1461
1475
|
var waiting = new Set;
|
|
1462
1476
|
var atExit = new Set;
|
|
1463
1477
|
function scratchDirectory(kind) {
|
|
1464
|
-
return mkdtempSync3(
|
|
1478
|
+
return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process4.pid}-`));
|
|
1465
1479
|
}
|
|
1466
1480
|
function discardWith(remove, paths) {
|
|
1467
1481
|
const left = [];
|
|
@@ -1921,8 +1935,8 @@ async function startDirectly(spec, output, hidden, detached = true, errors = out
|
|
|
1921
1935
|
}
|
|
1922
1936
|
async function startThroughHelper(spec, output, stopFile) {
|
|
1923
1937
|
const scratch = scratchDirectory("desktop");
|
|
1924
|
-
const pidFile =
|
|
1925
|
-
const errorFile =
|
|
1938
|
+
const pidFile = join7(scratch, "pid");
|
|
1939
|
+
const errorFile = join7(scratch, "error");
|
|
1926
1940
|
const where = spec.desktop === undefined ? "without activation" : `on the ${spec.desktop} desktop`;
|
|
1927
1941
|
try {
|
|
1928
1942
|
const compiled = await ensureHelperCompiled();
|
|
@@ -1930,7 +1944,7 @@ async function startThroughHelper(spec, output, stopFile) {
|
|
|
1930
1944
|
return { error: `it could not be started ${where}: ${compiled}` };
|
|
1931
1945
|
}
|
|
1932
1946
|
const wrapper = throughHelper({ ...spec, directory: process.cwd() }, spec.command, spec.args, {
|
|
1933
|
-
spec:
|
|
1947
|
+
spec: join7(scratch, "spec.json"),
|
|
1934
1948
|
pid: pidFile,
|
|
1935
1949
|
error: errorFile,
|
|
1936
1950
|
stop: stopFile
|
|
@@ -1974,7 +1988,7 @@ function profiledBy(path, projectPath) {
|
|
|
1974
1988
|
};
|
|
1975
1989
|
let projectGodot = null;
|
|
1976
1990
|
try {
|
|
1977
|
-
projectGodot = readFileSync3(
|
|
1991
|
+
projectGodot = readFileSync3(join7(projectPath, "project.godot"), "utf8");
|
|
1978
1992
|
} catch {}
|
|
1979
1993
|
const observer = new ProfileObserver((totals) => {
|
|
1980
1994
|
const first = latest === null;
|
|
@@ -2077,8 +2091,8 @@ async function keep() {
|
|
|
2077
2091
|
const debuggerPeer = run === undefined || run.debugged === false ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
|
|
2078
2092
|
const toStart = debuggerPeer === null ? spec : { ...spec, args: withEngineArguments(spec.args, debuggerPeer.engineArguments) };
|
|
2079
2093
|
const transcript = run === undefined ? "ignore" : openSync3(run.transcript, "a");
|
|
2080
|
-
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(
|
|
2081
|
-
const stopFile = own === null ? "" :
|
|
2094
|
+
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(join7(tmpdir5(), "gdharness-stop-")) : null;
|
|
2095
|
+
const stopFile = own === null ? "" : join7(own, "stop");
|
|
2082
2096
|
let started;
|
|
2083
2097
|
try {
|
|
2084
2098
|
started = await startDetached(toStart, transcript, run === undefined, stopFile);
|
|
@@ -2108,26 +2122,8 @@ async function keep() {
|
|
|
2108
2122
|
if (record !== null) {
|
|
2109
2123
|
writeNoteIfItCan(record);
|
|
2110
2124
|
}
|
|
2125
|
+
const exited = exitOf(child);
|
|
2111
2126
|
let listening = null;
|
|
2112
|
-
child.once("exit", (code, signal) => {
|
|
2113
|
-
const finish = () => {
|
|
2114
|
-
profiled?.finish();
|
|
2115
|
-
if (run !== undefined) {
|
|
2116
|
-
recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
2117
|
-
}
|
|
2118
|
-
debuggerPeer?.close();
|
|
2119
|
-
listening?.close();
|
|
2120
|
-
if (own !== null) {
|
|
2121
|
-
rmSync6(own, { recursive: true, force: true });
|
|
2122
|
-
}
|
|
2123
|
-
process.exit(0);
|
|
2124
|
-
};
|
|
2125
|
-
if (debuggerPeer === null) {
|
|
2126
|
-
finish();
|
|
2127
|
-
} else {
|
|
2128
|
-
debuggerPeer.drained(DRAIN_MS).then(finish);
|
|
2129
|
-
}
|
|
2130
|
-
});
|
|
2131
2127
|
if (record !== null) {
|
|
2132
2128
|
listening = await listenForTheServer(() => {
|
|
2133
2129
|
if (stopFile !== "") {
|
|
@@ -2140,9 +2136,27 @@ async function keep() {
|
|
|
2140
2136
|
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
2141
2137
|
}
|
|
2142
2138
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2139
|
+
await new Promise((resolve) => {
|
|
2140
|
+
process.stdout.once("error", () => {
|
|
2141
|
+
resolve();
|
|
2142
|
+
});
|
|
2143
|
+
process.stdout.end(`pid ${pid}
|
|
2144
|
+
`, resolve);
|
|
2145
|
+
});
|
|
2146
|
+
const { code, signal } = await exited;
|
|
2147
|
+
if (debuggerPeer !== null) {
|
|
2148
|
+
await debuggerPeer.drained(DRAIN_MS);
|
|
2149
|
+
}
|
|
2150
|
+
profiled?.finish();
|
|
2151
|
+
if (run !== undefined) {
|
|
2152
|
+
recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
2153
|
+
}
|
|
2154
|
+
debuggerPeer?.close();
|
|
2155
|
+
listening?.close();
|
|
2156
|
+
if (own !== null) {
|
|
2157
|
+
rmSync6(own, { recursive: true, force: true });
|
|
2158
|
+
}
|
|
2159
|
+
process.exit(0);
|
|
2146
2160
|
}
|
|
2147
2161
|
var mode = process.argv[2];
|
|
2148
2162
|
if (mode === "launch") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
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",
|