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 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: { effect: "destructive", summary: "save the scene as it is in the editor", requires: [] },
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: { effect: "read", summary: "read a node's properties", requires: ["nodePath"] },
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.pendingRequests.size,
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 shownSymbol(entry) {
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 = shownPosition(whole["end"]);
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.line },
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 alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
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 ${toolName}, 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"]);
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 ${toolName} with an error: ${message}`);
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.set(property, value[1])
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.get(property), given[property]):
182
- target.set(property, given[property])
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.get(property)
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. Anything else is a
611
- ## dictionary the property genuinely wants, and its values are read as values in their own right:
612
- ## kept as the tags they arrived as, a Vector2 read off one node and written to another was saved as
613
- ## a dictionary holding the word "Vector2".
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
- match expected_type:
616
- TYPE_VECTOR2:
617
- if value.has("x") and value.has("y"):
618
- return Vector2(Read.as_float(value["x"]), Read.as_float(value["y"]))
619
- TYPE_VECTOR2I:
620
- if value.has("x") and value.has("y"):
621
- return Vector2i(Read.as_int(value["x"]), Read.as_int(value["y"]))
622
- TYPE_VECTOR3:
623
- if value.has("x") and value.has("y") and value.has("z"):
624
- return Vector3(
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 = loaded.get(property)
130
- if not PropertyValues.same(read, resource.get(property)):
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 [false, "Source %d has no tile at %s" % [source_id, atlas_coords]]
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 = copy.get(property)
105
- if not scene.kept(saved, loaded, node.get(property)):
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, node.get(str(key)))
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
- return {
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].