gdharness 1.2.30 → 1.2.31
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 +78 -22
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +78 -3
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
- package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
- package/build/godot/operations/audio_buses.gd +14 -1
- package/build/godot/operations/class_cache.gd +146 -35
- package/build/godot/operations/classdb_queries.gd +19 -12
- package/build/godot/operations/project_config.gd +16 -6
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +78 -22
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -11153,7 +11153,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11153
11153
|
};
|
|
11154
11154
|
PROPERTIES = {
|
|
11155
11155
|
type: "object",
|
|
11156
|
-
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
11156
|
+
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
11157
11157
|
additionalProperties: true
|
|
11158
11158
|
};
|
|
11159
11159
|
XY = {
|
|
@@ -11608,7 +11608,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
11608
11608
|
requires: ["projectPath", "scenePath"],
|
|
11609
11609
|
operations: {
|
|
11610
11610
|
create: { effect: "destructive", summary: "a new scene with one root node", requires: [] },
|
|
11611
|
-
save: {
|
|
11611
|
+
save: {
|
|
11612
|
+
effect: "destructive",
|
|
11613
|
+
summary: "save the scene as it is in the editor, or as the file loads when the editor does not have it open; from says which",
|
|
11614
|
+
requires: []
|
|
11615
|
+
},
|
|
11612
11616
|
save_as: { effect: "destructive", summary: "save a copy under newPath", requires: ["newPath"] }
|
|
11613
11617
|
},
|
|
11614
11618
|
defaultOperation: "create"
|
|
@@ -11656,7 +11660,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
11656
11660
|
requires: ["projectPath", "scenePath"],
|
|
11657
11661
|
operations: {
|
|
11658
11662
|
add: { effect: "additive", summary: "add a node of any class", requires: ["nodeType", "nodeName"] },
|
|
11659
|
-
get: {
|
|
11663
|
+
get: {
|
|
11664
|
+
effect: "read",
|
|
11665
|
+
summary: "read a node's properties, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all",
|
|
11666
|
+
requires: ["nodePath"]
|
|
11667
|
+
},
|
|
11660
11668
|
set: {
|
|
11661
11669
|
effect: "destructive",
|
|
11662
11670
|
summary: "set properties on a node",
|
|
@@ -11683,7 +11691,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11683
11691
|
{
|
|
11684
11692
|
name: "scene_signal",
|
|
11685
11693
|
title: "Scene signals",
|
|
11686
|
-
description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. Needs the editor connected.",
|
|
11694
|
+
description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. Needs the editor connected.",
|
|
11687
11695
|
parameters: {
|
|
11688
11696
|
projectPath: PROJECT_PATH,
|
|
11689
11697
|
scenePath: SCENE_PATH,
|
|
@@ -39556,6 +39564,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
39556
39564
|
const info = this.connectionInfo;
|
|
39557
39565
|
return this.socket?.readyState === import_websocket.default.OPEN && info !== null && (info.greeted !== true || info.identifying === true);
|
|
39558
39566
|
}
|
|
39567
|
+
requestsInFlight() {
|
|
39568
|
+
return [...this.pendingRequests.values()].filter((pending) => pending.counted).map((pending) => ({ tool: pending.toolName, startedAt: pending.startedAt })).sort((a, b) => a.startedAt - b.startedAt);
|
|
39569
|
+
}
|
|
39559
39570
|
getStatus() {
|
|
39560
39571
|
const identifying = this.connectionInfo?.identifying === true;
|
|
39561
39572
|
const editor = identifying ? null : this.connectionInfo;
|
|
@@ -39577,18 +39588,19 @@ var init_godot_bridge = __esm(() => {
|
|
|
39577
39588
|
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
39578
39589
|
turnedAway: this.turnedAway ?? undefined,
|
|
39579
39590
|
identifying: identifying ? true : undefined,
|
|
39580
|
-
pendingRequests: this.
|
|
39591
|
+
pendingRequests: this.requestsInFlight().length,
|
|
39581
39592
|
queuedResources: this.resourceQueues.size,
|
|
39582
39593
|
listeningSince: this.listeningSince ?? undefined,
|
|
39583
39594
|
portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
|
|
39584
39595
|
};
|
|
39585
39596
|
}
|
|
39586
|
-
invokeTool(toolName, args) {
|
|
39597
|
+
invokeTool(toolName, args, options = {}) {
|
|
39598
|
+
const counted = options.counted ?? true;
|
|
39587
39599
|
const resourceKey = this.getResourceKey(args);
|
|
39588
39600
|
if (!resourceKey) {
|
|
39589
|
-
return this.invokeToolDirect(toolName, args);
|
|
39601
|
+
return this.invokeToolDirect(toolName, args, counted);
|
|
39590
39602
|
}
|
|
39591
|
-
return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, resourceKey));
|
|
39603
|
+
return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, counted, resourceKey));
|
|
39592
39604
|
}
|
|
39593
39605
|
getRequestPathname(url) {
|
|
39594
39606
|
try {
|
|
@@ -39740,7 +39752,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
39740
39752
|
return;
|
|
39741
39753
|
}
|
|
39742
39754
|
}
|
|
39743
|
-
invokeToolDirect(toolName, args, resourceKey) {
|
|
39755
|
+
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
39744
39756
|
if (!this.isConnected()) {
|
|
39745
39757
|
return Promise.reject(new Error("Godot is not connected"));
|
|
39746
39758
|
}
|
|
@@ -39762,7 +39774,8 @@ var init_godot_bridge = __esm(() => {
|
|
|
39762
39774
|
resolve,
|
|
39763
39775
|
reject,
|
|
39764
39776
|
startedAt: Date.now(),
|
|
39765
|
-
resourceKey
|
|
39777
|
+
resourceKey,
|
|
39778
|
+
counted
|
|
39766
39779
|
});
|
|
39767
39780
|
this.emitBridgeEvent("tool_start", {
|
|
39768
39781
|
tool: toolName,
|
|
@@ -41127,11 +41140,26 @@ function shownDiagnostic(entry) {
|
|
|
41127
41140
|
...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
|
|
41128
41141
|
};
|
|
41129
41142
|
}
|
|
41130
|
-
function
|
|
41143
|
+
function linesIn(content) {
|
|
41144
|
+
const lines = content.split(`
|
|
41145
|
+
`);
|
|
41146
|
+
return Math.max(1, content.endsWith(`
|
|
41147
|
+
`) ? lines.length - 1 : lines.length);
|
|
41148
|
+
}
|
|
41149
|
+
function lastLineOf(whole, lastOfDocument) {
|
|
41150
|
+
const end = shownPosition(whole["end"]);
|
|
41151
|
+
if (end === null) {
|
|
41152
|
+
return null;
|
|
41153
|
+
}
|
|
41154
|
+
const start = shownPosition(whole["start"]);
|
|
41155
|
+
const exclusive = end.column === 1 && (start === null || end.line > start.line);
|
|
41156
|
+
return Math.min(exclusive ? end.line - 1 : end.line, lastOfDocument);
|
|
41157
|
+
}
|
|
41158
|
+
function shownSymbol(entry, lastOfDocument) {
|
|
41131
41159
|
const symbol = asRecord(entry);
|
|
41132
41160
|
const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
|
|
41133
41161
|
const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
|
|
41134
|
-
const last =
|
|
41162
|
+
const last = lastLineOf(whole, lastOfDocument);
|
|
41135
41163
|
const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
|
|
41136
41164
|
const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
|
|
41137
41165
|
return {
|
|
@@ -41139,11 +41167,11 @@ function shownSymbol(entry) {
|
|
|
41139
41167
|
kind: named(symbol["kind"], SYMBOL_KINDS),
|
|
41140
41168
|
...typeof detail === "string" && detail !== "" ? { detail } : {},
|
|
41141
41169
|
...at === null ? {} : { line: at.line, column: at.column },
|
|
41142
|
-
...last === null ? {} : { lastLine: last
|
|
41170
|
+
...last === null ? {} : { lastLine: last },
|
|
41143
41171
|
...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
|
|
41144
41172
|
...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
|
|
41145
41173
|
...deprecated === true ? { deprecated } : {},
|
|
41146
|
-
...children.length > 0 ? { children: children.map(shownSymbol) } : {}
|
|
41174
|
+
...children.length > 0 ? { children: children.map((child) => shownSymbol(child, lastOfDocument)) } : {}
|
|
41147
41175
|
};
|
|
41148
41176
|
}
|
|
41149
41177
|
function typedBefore(content, line, character) {
|
|
@@ -41247,7 +41275,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
41247
41275
|
}
|
|
41248
41276
|
case "lsp_get_symbols": {
|
|
41249
41277
|
const symbols = await client.getDocumentSymbols(scriptPath, content);
|
|
41250
|
-
return asToolResponse({ symbols: symbols.map(shownSymbol) });
|
|
41278
|
+
return asToolResponse({ symbols: symbols.map((symbol) => shownSymbol(symbol, linesIn(content))) });
|
|
41251
41279
|
}
|
|
41252
41280
|
default:
|
|
41253
41281
|
return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
|
|
@@ -45472,7 +45500,7 @@ class GodotServer {
|
|
|
45472
45500
|
const { op: _op, ...arguments_ } = args;
|
|
45473
45501
|
const bridge = async (command, extra = {}) => {
|
|
45474
45502
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
45475
|
-
return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
|
|
45503
|
+
return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
|
|
45476
45504
|
};
|
|
45477
45505
|
switch (tool) {
|
|
45478
45506
|
case "project_info":
|
|
@@ -46202,8 +46230,30 @@ class GodotServer {
|
|
|
46202
46230
|
]
|
|
46203
46231
|
};
|
|
46204
46232
|
}
|
|
46233
|
+
const dropped = await this.optionsTheImporterDropped(project.value.path, params, written.payload);
|
|
46234
|
+
if (dropped !== null) {
|
|
46235
|
+
return dropped;
|
|
46236
|
+
}
|
|
46205
46237
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
46206
46238
|
}
|
|
46239
|
+
async optionsTheImporterDropped(projectPath, params, written) {
|
|
46240
|
+
const listed = written["updated_options"];
|
|
46241
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
46242
|
+
const { options: _options, ...where } = params;
|
|
46243
|
+
const reread = await this.operation("get_import_options", where, projectPath);
|
|
46244
|
+
if (!reread.ok || updated.length === 0) {
|
|
46245
|
+
return null;
|
|
46246
|
+
}
|
|
46247
|
+
const held = reread.payload["params"];
|
|
46248
|
+
const kept = held !== null && typeof held === "object" ? Object.keys(held) : [];
|
|
46249
|
+
const gone = updated.filter((name) => !kept.includes(name));
|
|
46250
|
+
if (gone.length === 0) {
|
|
46251
|
+
return null;
|
|
46252
|
+
}
|
|
46253
|
+
const applied = updated.filter((name) => kept.includes(name));
|
|
46254
|
+
const named = gone.join(", ");
|
|
46255
|
+
return this.createErrorResponse(`${named} ${gone.length === 1 ? "is not an option" : "are not options"} of this resource's importer: the reimport left ${gone.length === 1 ? "it" : "them"} out of the import file, so ${gone.length === 1 ? "it changed" : "they changed"} nothing.${applied.length === 0 ? "" : ` ${applied.join(", ")} ${applied.length === 1 ? "was" : "were"} applied.`} Its options are: ${kept.join(", ")}.`, ["project_import options lists every option with the value it holds"]);
|
|
46256
|
+
}
|
|
46207
46257
|
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
46208
46258
|
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
46209
46259
|
const before = await this.operation("get_import_status", ask, projectPath);
|
|
@@ -47303,7 +47353,7 @@ class GodotServer {
|
|
|
47303
47353
|
}
|
|
47304
47354
|
try {
|
|
47305
47355
|
const askedAt = Date.now();
|
|
47306
|
-
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
47356
|
+
const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
|
|
47307
47357
|
const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
|
|
47308
47358
|
if (answer === null) {
|
|
47309
47359
|
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
@@ -47328,7 +47378,7 @@ class GodotServer {
|
|
|
47328
47378
|
}
|
|
47329
47379
|
try {
|
|
47330
47380
|
const answer = await Promise.race([
|
|
47331
|
-
this.godotBridge.invokeTool("scan_status", {}),
|
|
47381
|
+
this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
|
|
47332
47382
|
delay4(PLAYING_STATUS_MS).then(() => null)
|
|
47333
47383
|
]);
|
|
47334
47384
|
if (answer === null) {
|
|
@@ -47396,10 +47446,14 @@ class GodotServer {
|
|
|
47396
47446
|
this.getEditorStatusPayload(),
|
|
47397
47447
|
this.editorServersListening()
|
|
47398
47448
|
]);
|
|
47449
|
+
const answeredAt = Date.now();
|
|
47450
|
+
const inFlight = this.godotBridge.requestsInFlight().map((request) => ({ tool: request.tool, forMs: answeredAt - request.startedAt }));
|
|
47399
47451
|
return this.jsonTextResponse({
|
|
47400
47452
|
editor: {
|
|
47401
47453
|
...editorStatus,
|
|
47402
47454
|
...servers,
|
|
47455
|
+
pendingRequests: inFlight.length,
|
|
47456
|
+
pendingRequestTools: inFlight.length === 0 ? undefined : inFlight,
|
|
47403
47457
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
|
|
47404
47458
|
scan
|
|
47405
47459
|
},
|
|
@@ -49987,14 +50041,16 @@ class GodotServer {
|
|
|
49987
50041
|
if (!(answered instanceof BridgeTimeout)) {
|
|
49988
50042
|
return answered;
|
|
49989
50043
|
}
|
|
49990
|
-
return this.createErrorResponse(`The editor took ${toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
|
|
50044
|
+
return this.createErrorResponse(`The editor took ${askedAs ?? toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
|
|
49991
50045
|
"editor_status says whether it is scanning or importing",
|
|
49992
50046
|
"The editor still carries the request out once it is free, so read what it changed before asking again"
|
|
49993
50047
|
]);
|
|
49994
50048
|
}
|
|
49995
50049
|
async askViaBridge(toolName, args, askedAs) {
|
|
50050
|
+
const named = askedAs ?? toolName;
|
|
49996
50051
|
if (!this.godotBridge.isConnected()) {
|
|
49997
|
-
const
|
|
50052
|
+
const tool = askedAs?.split(" ")[0];
|
|
50053
|
+
const alternative = tool !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, tool) ? EDITOR_FREE_INSTEAD[tool] : undefined;
|
|
49998
50054
|
const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
|
|
49999
50055
|
if (refused !== null) {
|
|
50000
50056
|
return refused;
|
|
@@ -50018,9 +50074,9 @@ class GodotServer {
|
|
|
50018
50074
|
if (message.includes(`Unknown tool: ${toolName}`)) {
|
|
50019
50075
|
const status = this.godotBridge.getStatus();
|
|
50020
50076
|
const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
50021
|
-
return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${
|
|
50077
|
+
return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${named}, so it cannot answer this call. ${remedy ?? "Restart it with editor_launch restart to load the addon the project holds."}`, ["editor_status says which addon the editor holds"]);
|
|
50022
50078
|
}
|
|
50023
|
-
return this.createErrorResponse(`The editor answered ${
|
|
50079
|
+
return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
|
|
50024
50080
|
}
|
|
50025
50081
|
}
|
|
50026
50082
|
runtimeTimeoutMs() {
|
|
@@ -162,6 +162,9 @@ func cannot_hold(
|
|
|
162
162
|
## max_value) is set once more after the rest, so the order a caller wrote them in does not decide
|
|
163
163
|
## what is saved.
|
|
164
164
|
##
|
|
165
|
+
## A name may be a colon path into a property's components, "position:x" or "region_rect:size:y",
|
|
166
|
+
## spelled as the runtime tools spell it, so one axis can be written without restating the others.
|
|
167
|
+
##
|
|
165
168
|
## Nothing here saves, so a refusal partway leaves the change in memory and the file as it was. The
|
|
166
169
|
## caller returns the refusal instead of saving.
|
|
167
170
|
func write_all(target: Object, properties: Dictionary) -> String:
|
|
@@ -169,19 +172,23 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
169
172
|
for key: Variant in properties:
|
|
170
173
|
var property: String = str(key)
|
|
171
174
|
var declared: Dictionary = declaration(target, property)
|
|
175
|
+
if declared.is_empty() and ":" in property:
|
|
176
|
+
declared = _component_slot(target, property)
|
|
177
|
+
if declared.has("refused"):
|
|
178
|
+
return str(declared["refused"])
|
|
172
179
|
if declared.is_empty():
|
|
173
180
|
return "%s has no property %s" % [target.get_class(), property]
|
|
174
181
|
var value: Array = value_for(target, declared, properties[key])
|
|
175
182
|
if not value[0]:
|
|
176
183
|
return str(value[1])
|
|
177
|
-
target
|
|
184
|
+
_write(target, property, value[1])
|
|
178
185
|
given[property] = value[1]
|
|
179
186
|
|
|
180
187
|
for property: String in given:
|
|
181
|
-
if not same(target
|
|
182
|
-
target
|
|
188
|
+
if not same(read_at(target, property), given[property]):
|
|
189
|
+
_write(target, property, given[property])
|
|
183
190
|
for property: String in given:
|
|
184
|
-
var held: Variant = target
|
|
191
|
+
var held: Variant = read_at(target, property)
|
|
185
192
|
if not same(held, given[property]):
|
|
186
193
|
return (
|
|
187
194
|
(
|
|
@@ -193,6 +200,58 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
193
200
|
return ""
|
|
194
201
|
|
|
195
202
|
|
|
203
|
+
## What [param target] holds under [param key], a property or a colon path into one's components.
|
|
204
|
+
func read_at(target: Object, key: String) -> Variant:
|
|
205
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
206
|
+
return target.get_indexed(NodePath(key))
|
|
207
|
+
return target.get(key)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Writes [param value] under [param key], putting a changed component back into the property it
|
|
211
|
+
## was read out of, and that one into the one above it.
|
|
212
|
+
func _write(target: Object, key: String, value: Variant) -> void:
|
|
213
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
214
|
+
target.set_indexed(NodePath(key), value)
|
|
215
|
+
else:
|
|
216
|
+
target.set(key, value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## The declaration the last step of [param path] would have, typed as the component it reaches, so
|
|
220
|
+
## a fraction for an integer vector's axis is refused as one for an int property is; or
|
|
221
|
+
## {"refused": why} for the first step that is not there.
|
|
222
|
+
func _component_slot(target: Object, path: String) -> Dictionary:
|
|
223
|
+
var steps: PackedStringArray = path.split(":")
|
|
224
|
+
var owner: String = target.get_class()
|
|
225
|
+
if declaration(target, steps[0]).is_empty():
|
|
226
|
+
return {"refused": "%s has no property %s" % [owner, steps[0]]}
|
|
227
|
+
var value: Variant = target.get(steps[0])
|
|
228
|
+
var walked: String = steps[0]
|
|
229
|
+
for index: int in range(1, steps.size()):
|
|
230
|
+
var step: String = steps[index]
|
|
231
|
+
var has: PackedStringArray = Serialisation.components_of(value)
|
|
232
|
+
var kind: String = type_string(typeof(value))
|
|
233
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
234
|
+
if has.is_empty():
|
|
235
|
+
return {
|
|
236
|
+
"refused":
|
|
237
|
+
(
|
|
238
|
+
(
|
|
239
|
+
"%s.%s is %s, which a path does not step into: a path reaches the components of a "
|
|
240
|
+
+ "vector, a colour, a rectangle or a transform."
|
|
241
|
+
)
|
|
242
|
+
% [owner, walked, sort]
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
if not step in has:
|
|
246
|
+
return {
|
|
247
|
+
"refused":
|
|
248
|
+
"%s.%s is %s, so it has no %s; it has %s" % [owner, walked, sort, step, ", ".join(has)]
|
|
249
|
+
}
|
|
250
|
+
value = value[step]
|
|
251
|
+
walked += ":" + step
|
|
252
|
+
return {"name": path, "type": typeof(value), "usage": PROPERTY_USAGE_DEFAULT}
|
|
253
|
+
|
|
254
|
+
|
|
196
255
|
## [param raw] as the value [param declared] takes, as [ok, value] or [false, why not].
|
|
197
256
|
func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
198
257
|
var property: String = str(declared.get("name", ""))
|
|
@@ -607,57 +666,24 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
|
607
666
|
return [true, built]
|
|
608
667
|
|
|
609
668
|
|
|
610
|
-
## A dictionary with no tag, read against the type the property declares
|
|
611
|
-
##
|
|
612
|
-
##
|
|
613
|
-
## a
|
|
669
|
+
## A dictionary with no tag, read against the type the property declares, by the fields the tagged
|
|
670
|
+
## form of that type carries: the shape a read answers with, untagged, is a value too. A rectangle
|
|
671
|
+
## written as the position and size a read gave was refused while the four numbers were taken, and
|
|
672
|
+
## a Rect2i, an AABB or a transform had no untagged form at all. Anything else is a dictionary the
|
|
673
|
+
## property genuinely wants, and its values are read as values in their own right: kept as the tags
|
|
674
|
+
## they arrived as, a Vector2 read off one node and written to another was saved as a dictionary
|
|
675
|
+
## holding the word "Vector2".
|
|
614
676
|
func _parse_shaped_dictionary(value: Dictionary, expected_type: int, elements: Array) -> Variant:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
Read.as_float(value["x"]), Read.as_float(value["y"]), Read.as_float(value["z"])
|
|
626
|
-
)
|
|
627
|
-
TYPE_VECTOR3I:
|
|
628
|
-
if value.has("x") and value.has("y") and value.has("z"):
|
|
629
|
-
return Vector3i(Read.as_int(value["x"]), Read.as_int(value["y"]), Read.as_int(value["z"]))
|
|
630
|
-
TYPE_VECTOR4:
|
|
631
|
-
if value.has("x") and value.has("y") and value.has("z") and value.has("w"):
|
|
632
|
-
return Vector4(
|
|
633
|
-
Read.as_float(value["x"]),
|
|
634
|
-
Read.as_float(value["y"]),
|
|
635
|
-
Read.as_float(value["z"]),
|
|
636
|
-
Read.as_float(value["w"])
|
|
637
|
-
)
|
|
638
|
-
TYPE_COLOR:
|
|
639
|
-
if value.has("r") and value.has("g") and value.has("b"):
|
|
640
|
-
return Color(
|
|
641
|
-
Read.as_float(value["r"]),
|
|
642
|
-
Read.as_float(value["g"]),
|
|
643
|
-
Read.as_float(value["b"]),
|
|
644
|
-
Read.as_float(value.get("a", 1), 1.0)
|
|
645
|
-
)
|
|
646
|
-
TYPE_RECT2:
|
|
647
|
-
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
648
|
-
return Rect2(
|
|
649
|
-
Read.as_float(value["x"]),
|
|
650
|
-
Read.as_float(value["y"]),
|
|
651
|
-
Read.as_float(value["width"]),
|
|
652
|
-
Read.as_float(value["height"])
|
|
653
|
-
)
|
|
654
|
-
TYPE_NODE_PATH:
|
|
655
|
-
if value.has("path"):
|
|
656
|
-
return NodePath(str(value["path"]))
|
|
657
|
-
if expected_type != TYPE_NIL and expected_type != TYPE_DICTIONARY:
|
|
658
|
-
return value
|
|
659
|
-
|
|
660
|
-
return _parse_entries(value, elements)
|
|
677
|
+
if expected_type == TYPE_NIL or expected_type == TYPE_DICTIONARY:
|
|
678
|
+
return _parse_entries(value, elements)
|
|
679
|
+
var tag: String = type_string(expected_type)
|
|
680
|
+
if Serialisation.REQUIRED.has(tag):
|
|
681
|
+
var tagged: Dictionary = value.duplicate()
|
|
682
|
+
tagged["_type"] = tag
|
|
683
|
+
var built: Variant = _values.deserialize_value(tagged)
|
|
684
|
+
if typeof(built) == expected_type:
|
|
685
|
+
return built
|
|
686
|
+
return value
|
|
661
687
|
|
|
662
688
|
|
|
663
689
|
## The entries of a dictionary a property holds, each value read as a value in its own right, and
|
|
@@ -246,6 +246,52 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
246
246
|
return call(builder, fields)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
|
+
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
|
+
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
252
|
+
## into a component is a write into the copy until it is put back where it was read from.
|
|
253
|
+
static func components_of(value: Variant) -> PackedStringArray:
|
|
254
|
+
match typeof(value):
|
|
255
|
+
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
256
|
+
return PackedStringArray(["x", "y"])
|
|
257
|
+
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
258
|
+
return PackedStringArray(["x", "y", "z"])
|
|
259
|
+
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
260
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
261
|
+
TYPE_COLOR:
|
|
262
|
+
return PackedStringArray(
|
|
263
|
+
[
|
|
264
|
+
"r",
|
|
265
|
+
"g",
|
|
266
|
+
"b",
|
|
267
|
+
"a",
|
|
268
|
+
"r8",
|
|
269
|
+
"g8",
|
|
270
|
+
"b8",
|
|
271
|
+
"a8",
|
|
272
|
+
"h",
|
|
273
|
+
"s",
|
|
274
|
+
"v",
|
|
275
|
+
"ok_hsl_h",
|
|
276
|
+
"ok_hsl_s",
|
|
277
|
+
"ok_hsl_l"
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
281
|
+
return PackedStringArray(["position", "size", "end"])
|
|
282
|
+
TYPE_PLANE:
|
|
283
|
+
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
284
|
+
TYPE_BASIS:
|
|
285
|
+
return PackedStringArray(["x", "y", "z"])
|
|
286
|
+
TYPE_TRANSFORM2D:
|
|
287
|
+
return PackedStringArray(["x", "y", "origin"])
|
|
288
|
+
TYPE_TRANSFORM3D:
|
|
289
|
+
return PackedStringArray(["basis", "origin"])
|
|
290
|
+
TYPE_PROJECTION:
|
|
291
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
292
|
+
return PackedStringArray()
|
|
293
|
+
|
|
294
|
+
|
|
249
295
|
## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
|
|
250
296
|
static func pairs(entries: Variant) -> bool:
|
|
251
297
|
if not entries is Array:
|
|
@@ -18,6 +18,9 @@ const SHADER_ENTRY: Dictionary[String, String] = {
|
|
|
18
18
|
"canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
|
|
22
|
+
const CELLS_SHOWN: int = 1000
|
|
23
|
+
|
|
21
24
|
var _editor_plugin: EditorPlugin = null
|
|
22
25
|
var _properties: PropertyValues = PropertyValues.new()
|
|
23
26
|
var _values: Serialisation = Serialisation.new()
|
|
@@ -126,8 +129,8 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
|
|
|
126
129
|
else:
|
|
127
130
|
for key: Variant in names:
|
|
128
131
|
var property: String = str(key)
|
|
129
|
-
var read: Variant =
|
|
130
|
-
if not PropertyValues.same(read,
|
|
132
|
+
var read: Variant = _properties.read_at(loaded, property)
|
|
133
|
+
if not PropertyValues.same(read, _properties.read_at(resource, property)):
|
|
131
134
|
lost = (
|
|
132
135
|
(
|
|
133
136
|
"%s.%s would load from the file as %s: it is not a property the file keeps, so "
|
|
@@ -552,7 +555,13 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
|
552
555
|
var atlas_coords: Vector2i = atlas_at[1]
|
|
553
556
|
var alternative: int = Read.as_int(cell.get("alternativeTile", 0))
|
|
554
557
|
if not source.has_tile(atlas_coords):
|
|
555
|
-
return [
|
|
558
|
+
return [
|
|
559
|
+
false,
|
|
560
|
+
(
|
|
561
|
+
"cells[%d]: source %d has no tile at %s; it has %s. Nothing was placed."
|
|
562
|
+
% [index, source_id, atlas_coords, _tiles_of(source)]
|
|
563
|
+
)
|
|
564
|
+
]
|
|
556
565
|
if not source.has_alternative_tile(atlas_coords, alternative):
|
|
557
566
|
return [
|
|
558
567
|
false,
|
|
@@ -561,6 +570,72 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
|
561
570
|
return [true, at[1], source_id, atlas_coords, alternative]
|
|
562
571
|
|
|
563
572
|
|
|
573
|
+
## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
|
|
574
|
+
## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
|
|
575
|
+
static func _tiles_of(source: TileSetSource) -> String:
|
|
576
|
+
var count: int = source.get_tiles_count()
|
|
577
|
+
if count == 0:
|
|
578
|
+
return "no tiles"
|
|
579
|
+
var named: Array[String] = []
|
|
580
|
+
for index: int in mini(count, 12):
|
|
581
|
+
named.append(str(source.get_tile_id(index)))
|
|
582
|
+
var more: String = "" if count <= 12 else " and %d more" % (count - 12)
|
|
583
|
+
return "tiles at " + ", ".join(PackedStringArray(named)) + more
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
|
|
587
|
+
## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
|
|
588
|
+
## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
|
|
589
|
+
## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
|
|
590
|
+
static func cells_of(node: Node) -> Dictionary:
|
|
591
|
+
var listed: Array[Dictionary] = []
|
|
592
|
+
var count: int = 0
|
|
593
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
594
|
+
var layered: TileMap = node as TileMap
|
|
595
|
+
if single != null:
|
|
596
|
+
var used: Array[Vector2i] = single.get_used_cells()
|
|
597
|
+
count = used.size()
|
|
598
|
+
for at: Vector2i in used.slice(0, CELLS_SHOWN):
|
|
599
|
+
listed.append(
|
|
600
|
+
_cell_entry(
|
|
601
|
+
at,
|
|
602
|
+
single.get_cell_source_id(at),
|
|
603
|
+
single.get_cell_atlas_coords(at),
|
|
604
|
+
single.get_cell_alternative_tile(at)
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
elif layered != null:
|
|
608
|
+
for layer: int in layered.get_layers_count():
|
|
609
|
+
var used: Array[Vector2i] = layered.get_used_cells(layer)
|
|
610
|
+
count += used.size()
|
|
611
|
+
for at: Vector2i in used:
|
|
612
|
+
if listed.size() >= CELLS_SHOWN:
|
|
613
|
+
break
|
|
614
|
+
var entry: Dictionary = _cell_entry(
|
|
615
|
+
at,
|
|
616
|
+
layered.get_cell_source_id(layer, at),
|
|
617
|
+
layered.get_cell_atlas_coords(layer, at),
|
|
618
|
+
layered.get_cell_alternative_tile(layer, at)
|
|
619
|
+
)
|
|
620
|
+
entry["layer"] = layer
|
|
621
|
+
listed.append(entry)
|
|
622
|
+
else:
|
|
623
|
+
return {}
|
|
624
|
+
var answer: Dictionary = {"cellCount": count, "cells": listed}
|
|
625
|
+
if count > listed.size():
|
|
626
|
+
answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
|
|
627
|
+
return answer
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
static func _cell_entry(at: Vector2i, source_id: int, atlas: Vector2i, alternative: int) -> Dictionary:
|
|
631
|
+
return {
|
|
632
|
+
"coords": {"x": at.x, "y": at.y},
|
|
633
|
+
"sourceId": source_id,
|
|
634
|
+
"atlasCoords": {"x": atlas.x, "y": atlas.y},
|
|
635
|
+
"alternativeTile": alternative,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
|
|
564
639
|
func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
|
|
565
640
|
var layered: TileMap = node as TileMap
|
|
566
641
|
if layered != null:
|
|
@@ -5,6 +5,7 @@ extends Node
|
|
|
5
5
|
|
|
6
6
|
const Read = preload("../reading.gd")
|
|
7
7
|
const PropertyValues = preload("../property_values.gd")
|
|
8
|
+
const ResourceTools = preload("resource_tools.gd")
|
|
8
9
|
const SceneFile = preload("../scene_file.gd")
|
|
9
10
|
const Serialisation = preload("../serialisation.gd")
|
|
10
11
|
|
|
@@ -101,8 +102,8 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
|
|
|
101
102
|
return "%s is not in the scene as it would be saved." % at
|
|
102
103
|
for key: Variant in names:
|
|
103
104
|
var property: String = str(key)
|
|
104
|
-
var loaded: Variant =
|
|
105
|
-
if not scene.kept(saved, loaded,
|
|
105
|
+
var loaded: Variant = _properties.read_at(copy, property)
|
|
106
|
+
if not scene.kept(saved, loaded, _properties.read_at(node, property)):
|
|
106
107
|
return (
|
|
107
108
|
(
|
|
108
109
|
"%s.%s would load from the file as %s: the scene does not keep it, because the property "
|
|
@@ -601,7 +602,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
601
602
|
|
|
602
603
|
var held: Dictionary = {}
|
|
603
604
|
for key: Variant in properties:
|
|
604
|
-
held[str(key)] = _readable(node,
|
|
605
|
+
held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
|
|
605
606
|
var new_path: String = scene.path_of(node)
|
|
606
607
|
var written: Dictionary = scene.write(
|
|
607
608
|
func(saved: Node) -> String: return _kept(scene, saved, node, properties.keys())
|
|
@@ -633,6 +634,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
633
634
|
# variable read as changed; and a value this scene sets back to the default was hidden, so the
|
|
634
635
|
# names this scene's file sets are always answered.
|
|
635
636
|
var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
|
|
637
|
+
var cells: Dictionary = ResourceTools.cells_of(node)
|
|
636
638
|
scene.close()
|
|
637
639
|
var answer: Dictionary = {
|
|
638
640
|
"ok": true,
|
|
@@ -640,6 +642,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
640
642
|
"properties": props,
|
|
641
643
|
"unsavedInEditor": SceneFile.unsaved(scene_path)
|
|
642
644
|
}
|
|
645
|
+
answer.merge(cells)
|
|
643
646
|
# An empty answer read as a node with nothing on it, rather than one with nothing changed.
|
|
644
647
|
if props.is_empty() and not include_defaults:
|
|
645
648
|
answer["note"] = (
|
|
@@ -757,6 +760,10 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
757
760
|
# asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
|
|
758
761
|
var callable: Callable = Callable(target, method_name)
|
|
759
762
|
var method_found: bool = target.has_method(method_name)
|
|
763
|
+
var flags_before: int = -1
|
|
764
|
+
for connection: Dictionary in source.get_signal_connection_list(signal_name):
|
|
765
|
+
if connection.get("callable") == callable:
|
|
766
|
+
flags_before = Read.as_int(connection.get("flags", 0))
|
|
760
767
|
if source.is_connected(signal_name, callable):
|
|
761
768
|
source.disconnect(signal_name, callable)
|
|
762
769
|
var connect_result: Error = source.connect(signal_name, callable, flags)
|
|
@@ -776,7 +783,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
776
783
|
if not written.is_empty():
|
|
777
784
|
return written
|
|
778
785
|
|
|
779
|
-
|
|
786
|
+
var answer: Dictionary = {
|
|
780
787
|
"ok": true,
|
|
781
788
|
"sourceNodePath": from,
|
|
782
789
|
"signalName": signal_name,
|
|
@@ -788,6 +795,14 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
788
795
|
# next; a caller told nothing learnt of a misspelt one only when the signal fired.
|
|
789
796
|
"methodFound": method_found
|
|
790
797
|
}
|
|
798
|
+
# A second connect of the same pair read exactly like the first, so a caller could not tell a
|
|
799
|
+
# connection made from one that was already there, and a list showing one where two were made
|
|
800
|
+
# looked like a lost write.
|
|
801
|
+
if flags_before >= 0:
|
|
802
|
+
answer["alreadyConnected"] = true
|
|
803
|
+
if flags_before != Read.as_int(saved_flags[0]):
|
|
804
|
+
answer["flagsBefore"] = _flag_names(flags_before)
|
|
805
|
+
return answer
|
|
791
806
|
|
|
792
807
|
|
|
793
808
|
## The two ends of a connection in [param scene], as [source, target], or [null, why not].
|