gdharness 1.3.1 → 1.3.3
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 +369 -97
- package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -9
- package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +5 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +161 -17
- package/build/godot/addons/gdharness_editor/reading.gd +37 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +3 -1
- package/build/godot/addons/gdharness_editor/serialisation.gd +4 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +367 -23
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +133 -39
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +20 -3
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +17 -13
- package/build/godot/addons/gdharness_editor/value_shapes.gd +7 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +37 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +27 -1
- package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +7 -0
- package/build/godot/operations/animation_reads.gd +11 -9
- package/build/godot/operations/classdb_queries.gd +13 -3
- package/build/godot/operations/declared_values.gd +116 -0
- package/build/godot/operations/dependencies.gd +109 -5
- package/build/godot/operations/editor_tools.gd +3 -0
- package/build/godot/operations/export_presets.gd +4 -6
- package/build/godot/operations/file_reads.gd +5 -0
- package/build/godot/operations/file_walk.gd +35 -0
- package/build/godot/operations/gdscript_analysis.gd +7 -2
- package/build/godot/operations/gdscript_authoring.gd +105 -2
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/import_pipeline.gd +39 -0
- package/build/godot/operations/input_actions.gd +73 -1
- package/build/godot/operations/project_config.gd +59 -3
- package/build/godot/operations/project_diagnostics.gd +27 -2
- package/build/godot/operations/property_values.gd +161 -17
- package/build/godot/operations/reading.gd +37 -0
- package/build/godot/operations/resource_files.gd +4 -1
- package/build/godot/operations/script_check.gd +16 -0
- package/build/godot/operations/serialisation.gd +4 -0
- package/build/godot/operations/value_shapes.gd +7 -0
- package/package.json +1 -1
|
@@ -252,7 +252,7 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
252
252
|
if not folder.is_empty():
|
|
253
253
|
return {"ok": false, "error": folder}
|
|
254
254
|
var answer: Dictionary = {"ok": true, "resourcePath": res_path, "resourceType": asked_type}
|
|
255
|
-
_properties.
|
|
255
|
+
_properties.note_write(answer)
|
|
256
256
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
257
257
|
if not failed.is_empty():
|
|
258
258
|
return {"ok": false, "error": failed}
|
|
@@ -290,7 +290,7 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
290
290
|
if not refused.is_empty():
|
|
291
291
|
return {"ok": false, "error": refused}
|
|
292
292
|
var answer: Dictionary = {"ok": true, "resourcePath": res_path}
|
|
293
|
-
_properties.
|
|
293
|
+
_properties.note_write(answer)
|
|
294
294
|
var previous: PackedByteArray = FileAccess.get_file_as_bytes(res_path)
|
|
295
295
|
var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
|
|
296
296
|
if not failed.is_empty():
|
|
@@ -346,7 +346,19 @@ func create_tileset(args: Dictionary) -> Dictionary:
|
|
|
346
346
|
var tileset_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
347
347
|
if tileset_path == "res://":
|
|
348
348
|
return {"ok": false, "error": "resourcePath is required"}
|
|
349
|
-
|
|
349
|
+
# modify sets properties and builds no atlas, so the other path is the only way on offered.
|
|
350
|
+
var taken: String = (
|
|
351
|
+
""
|
|
352
|
+
if not FileAccess.file_exists(tileset_path)
|
|
353
|
+
else (
|
|
354
|
+
(
|
|
355
|
+
"%s already exists, and create_tileset builds a whole new TileSet, which would replace "
|
|
356
|
+
+ "everything in it, the tiles a scene paints with among them. Give resourcePath a path "
|
|
357
|
+
+ "nothing is at."
|
|
358
|
+
)
|
|
359
|
+
% tileset_path
|
|
360
|
+
)
|
|
361
|
+
)
|
|
350
362
|
if not taken.is_empty():
|
|
351
363
|
return {"ok": false, "error": taken}
|
|
352
364
|
|
|
@@ -374,9 +386,16 @@ func create_tileset(args: Dictionary) -> Dictionary:
|
|
|
374
386
|
"ok": false,
|
|
375
387
|
"error": "The TileSet did not take the atlas for %s" % atlas.texture.resource_path
|
|
376
388
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
389
|
+
var entry: Dictionary = {
|
|
390
|
+
"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()
|
|
391
|
+
}
|
|
392
|
+
var blank: Array = built[2]
|
|
393
|
+
if not blank.is_empty():
|
|
394
|
+
entry["leftBlank"] = blank
|
|
395
|
+
var spare: String = built[3]
|
|
396
|
+
if not spare.is_empty():
|
|
397
|
+
entry["notInAnyTile"] = spare
|
|
398
|
+
made.append(entry)
|
|
380
399
|
|
|
381
400
|
var folder: String = GdhSceneFile.make_folder_for(tileset_path)
|
|
382
401
|
if not folder.is_empty():
|
|
@@ -389,11 +408,14 @@ func create_tileset(args: Dictionary) -> Dictionary:
|
|
|
389
408
|
return {"ok": true, "tilesetPath": tileset_path, "sources": made}
|
|
390
409
|
|
|
391
410
|
|
|
392
|
-
## One atlas source from its description, with its tiles made, as [true, source
|
|
411
|
+
## One atlas source from its description, with its tiles made, as [true, source, the grid cells left
|
|
412
|
+
## without a tile for being wholly transparent, a sentence on the pixels no tile covers or ""] or
|
|
413
|
+
## [false, why].
|
|
393
414
|
##
|
|
394
415
|
## Tiles are made in every cell of the grid that has a pixel that is not transparent, which is
|
|
395
416
|
## what the editor offers to do when an atlas gets a texture. Without them the atlas held no tiles,
|
|
396
|
-
## and every cell painted from it drew nothing.
|
|
417
|
+
## and every cell painted from it drew nothing. The cells passed over and the strip a tile size that
|
|
418
|
+
## does not divide the picture leaves are said, since a count alone left a caller to work out which.
|
|
397
419
|
func _atlas(entry: Variant, index: int) -> Array:
|
|
398
420
|
if not entry is Dictionary:
|
|
399
421
|
return [false, "sources[%d] must be an object with texture and tileSize" % index]
|
|
@@ -455,6 +477,7 @@ func _atlas(entry: Variant, index: int) -> Array:
|
|
|
455
477
|
)
|
|
456
478
|
]
|
|
457
479
|
var image: Image = texture.get_image()
|
|
480
|
+
var blank: Array = []
|
|
458
481
|
for y: int in grid.y:
|
|
459
482
|
for x: int in grid.x:
|
|
460
483
|
var cell: Vector2i = Vector2i(x, y)
|
|
@@ -464,9 +487,33 @@ func _atlas(entry: Variant, index: int) -> Array:
|
|
|
464
487
|
atlas.texture_region_size
|
|
465
488
|
)
|
|
466
489
|
if image != null and _blank(image, region):
|
|
490
|
+
blank.append({"x": x, "y": y})
|
|
467
491
|
continue
|
|
468
492
|
atlas.create_tile(cell)
|
|
469
|
-
|
|
493
|
+
var covered: Vector2i = atlas.margins + grid * tile_size + (grid - Vector2i.ONE) * atlas.separation
|
|
494
|
+
var left: Vector2i = Vector2i(texture.get_size()) - covered
|
|
495
|
+
var sides: PackedStringArray = []
|
|
496
|
+
if left.x > 0:
|
|
497
|
+
var _right: bool = sides.append("%d px on the right" % left.x)
|
|
498
|
+
if left.y > 0:
|
|
499
|
+
var _bottom: bool = sides.append("%d px at the bottom" % left.y)
|
|
500
|
+
var spare: String = (
|
|
501
|
+
""
|
|
502
|
+
if sides.is_empty()
|
|
503
|
+
else (
|
|
504
|
+
"%s is %d by %d, so %s %s in no %d by %d tile."
|
|
505
|
+
% [
|
|
506
|
+
texture_path,
|
|
507
|
+
texture.get_width(),
|
|
508
|
+
texture.get_height(),
|
|
509
|
+
" and ".join(sides),
|
|
510
|
+
"is" if sides.size() == 1 else "are",
|
|
511
|
+
tile_size.x,
|
|
512
|
+
tile_size.y
|
|
513
|
+
]
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return [true, atlas, blank, spare]
|
|
470
517
|
|
|
471
518
|
|
|
472
519
|
func _blank(image: Image, region: Rect2i) -> bool:
|
|
@@ -475,12 +522,15 @@ func _blank(image: Image, region: Rect2i) -> bool:
|
|
|
475
522
|
return image.get_region(region).is_invisible()
|
|
476
523
|
|
|
477
524
|
|
|
478
|
-
## The
|
|
479
|
-
|
|
480
|
-
|
|
525
|
+
## The sources a tile set holds, as "source 0", "sources 0, 2" or "no sources", for saying what a
|
|
526
|
+
## cell could have named instead.
|
|
527
|
+
func _sources_of(tile_set: TileSet) -> String:
|
|
528
|
+
var ids: PackedStringArray = []
|
|
481
529
|
for index: int in tile_set.get_source_count():
|
|
482
|
-
ids.append(tile_set.get_source_id(index))
|
|
483
|
-
|
|
530
|
+
var _listed: bool = ids.append(str(tile_set.get_source_id(index)))
|
|
531
|
+
if ids.is_empty():
|
|
532
|
+
return "no sources"
|
|
533
|
+
return ("source " if ids.size() == 1 else "sources ") + ", ".join(ids)
|
|
484
534
|
|
|
485
535
|
|
|
486
536
|
func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
@@ -517,11 +567,20 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
517
567
|
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
518
568
|
|
|
519
569
|
scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
|
|
520
|
-
|
|
570
|
+
# Every cell is checked before any is placed, and every one refused is named: stopping at the first
|
|
571
|
+
# left a caller fixing one cell a call.
|
|
572
|
+
var checked: Array = []
|
|
573
|
+
var refusals: PackedStringArray = []
|
|
521
574
|
for index: int in placed.size():
|
|
522
575
|
var cell: Array = _cell(placed[index], index, tile_set)
|
|
523
576
|
if not cell[0]:
|
|
524
|
-
|
|
577
|
+
var _named: bool = refusals.append(str(cell[1]))
|
|
578
|
+
checked.append(cell)
|
|
579
|
+
if not refusals.is_empty():
|
|
580
|
+
return scene.refuse("%s. Nothing was placed." % "; ".join(refusals))
|
|
581
|
+
var wanted: Array = []
|
|
582
|
+
var erased: int = 0
|
|
583
|
+
for cell: Array in checked:
|
|
525
584
|
var at: Vector2i = cell[1]
|
|
526
585
|
var source_id: int = cell[2]
|
|
527
586
|
var atlas_coords: Vector2i = cell[3]
|
|
@@ -531,6 +590,8 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
531
590
|
else:
|
|
532
591
|
single.set_cell(at, source_id, atlas_coords, alternative)
|
|
533
592
|
wanted.append([at, source_id, atlas_coords, alternative])
|
|
593
|
+
if source_id == -1:
|
|
594
|
+
erased += 1
|
|
534
595
|
|
|
535
596
|
var at_path: String = scene.path_of(node)
|
|
536
597
|
var written: Dictionary = scene.write(
|
|
@@ -546,7 +607,13 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
|
546
607
|
if not written.is_empty():
|
|
547
608
|
return written
|
|
548
609
|
|
|
549
|
-
return {
|
|
610
|
+
return {
|
|
611
|
+
"ok": true,
|
|
612
|
+
"layer": layer,
|
|
613
|
+
"placed": wanted.size() - erased,
|
|
614
|
+
"erased": erased,
|
|
615
|
+
"requested": placed.size()
|
|
616
|
+
}
|
|
550
617
|
|
|
551
618
|
|
|
552
619
|
## The node at [param node_path] cells are painted on, with a TileSet, as [node, why not].
|
|
@@ -575,40 +642,55 @@ func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
|
|
|
575
642
|
|
|
576
643
|
|
|
577
644
|
## One cell from its description, checked against [param tile_set], as
|
|
578
|
-
## [true, coords, source id, atlas coords, alternative] or [false, why].
|
|
645
|
+
## [true, coords, source id, atlas coords, alternative] or [false, why]. A source of -1 erases the
|
|
646
|
+
## cell, as set_cell takes it, and needs no atlasCoords.
|
|
579
647
|
func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
580
648
|
if not entry is Dictionary:
|
|
581
649
|
return [false, "cells[%d] must be an object with coords, sourceId and atlasCoords" % index]
|
|
582
650
|
var cell: Dictionary = entry
|
|
583
|
-
for field: String in ["coords", "sourceId"
|
|
651
|
+
for field: String in ["coords", "sourceId"]:
|
|
584
652
|
if not cell.has(field):
|
|
585
653
|
return [false, "cells[%d] has no %s" % [index, field]]
|
|
586
654
|
var at: Array = _xy(cell["coords"], "cells[%d].coords" % index)
|
|
587
655
|
if not at[0]:
|
|
588
656
|
return at
|
|
657
|
+
var coords: Vector2i = at[1]
|
|
658
|
+
var named: String = "cells[%d] at (%d, %d)" % [index, coords.x, coords.y]
|
|
659
|
+
var source_id: int = GdhRead.as_int(cell["sourceId"], -2)
|
|
660
|
+
if source_id == -1:
|
|
661
|
+
return [true, coords, -1, Vector2i(-1, -1), -1]
|
|
662
|
+
if not cell.has("atlasCoords"):
|
|
663
|
+
return [
|
|
664
|
+
false, "%s has no atlasCoords, which every cell but an erased one (sourceId -1) needs" % named
|
|
665
|
+
]
|
|
589
666
|
var atlas_at: Array = _xy(cell["atlasCoords"], "cells[%d].atlasCoords" % index)
|
|
590
667
|
if not atlas_at[0]:
|
|
591
668
|
return atlas_at
|
|
592
|
-
var source_id: int = GdhRead.as_int(cell["sourceId"], -1)
|
|
593
669
|
if not tile_set.has_source(source_id):
|
|
594
|
-
return [
|
|
670
|
+
return [
|
|
671
|
+
false,
|
|
672
|
+
(
|
|
673
|
+
"%s names source %d, and the TileSet has %s; sourceId -1 erases a cell"
|
|
674
|
+
% [named, source_id, _sources_of(tile_set)]
|
|
675
|
+
)
|
|
676
|
+
]
|
|
595
677
|
var source: TileSetSource = tile_set.get_source(source_id)
|
|
596
678
|
var atlas_coords: Vector2i = atlas_at[1]
|
|
597
679
|
var alternative: int = GdhRead.as_int(cell.get("alternativeTile", 0))
|
|
598
680
|
if not source.has_tile(atlas_coords):
|
|
599
681
|
return [
|
|
600
682
|
false,
|
|
601
|
-
(
|
|
602
|
-
"cells[%d]: source %d has no tile at %s; it has %s. Nothing was placed."
|
|
603
|
-
% [index, source_id, atlas_coords, _tiles_of(source)]
|
|
604
|
-
)
|
|
683
|
+
"%s: source %d has no tile at %s; it has %s" % [named, source_id, atlas_coords, _tiles_of(source)]
|
|
605
684
|
]
|
|
606
685
|
if not source.has_alternative_tile(atlas_coords, alternative):
|
|
607
686
|
return [
|
|
608
687
|
false,
|
|
609
|
-
|
|
688
|
+
(
|
|
689
|
+
"%s: the tile at %s in source %d has no alternative %d"
|
|
690
|
+
% [named, atlas_coords, source_id, alternative]
|
|
691
|
+
)
|
|
610
692
|
]
|
|
611
|
-
return [true,
|
|
693
|
+
return [true, coords, source_id, atlas_coords, alternative]
|
|
612
694
|
|
|
613
695
|
|
|
614
696
|
## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
|
|
@@ -722,13 +804,12 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
722
804
|
if theme == null:
|
|
723
805
|
return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
|
|
724
806
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
)
|
|
807
|
+
# Read as a property write reads a colour, html code included, so a value scene_node takes for a
|
|
808
|
+
# Button's modulate is taken for its theme colour too; "#ff8000" was refused here alone.
|
|
809
|
+
var parsed: Array = _properties.colour_from(args.get("color", {}), "color")
|
|
810
|
+
if not parsed[0]:
|
|
811
|
+
return {"ok": false, "error": parsed[1]}
|
|
812
|
+
var color: Color = parsed[1]
|
|
732
813
|
theme.set_color(color_name, control_type, color)
|
|
733
814
|
|
|
734
815
|
var unsaved: String = _save_theme(theme, theme_path, "%s/colors/%s" % [control_type, color_name])
|
|
@@ -768,17 +849,18 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
768
849
|
# Godot reads a size of 0 or less as unset and answers the theme's default, so -3 would be read
|
|
769
850
|
# back as 16.
|
|
770
851
|
var pixels: float = GdhRead.as_float(args.get("size"), 0.0)
|
|
771
|
-
if not is_finite(pixels) or pixels
|
|
772
|
-
|
|
852
|
+
if not is_finite(pixels) or pixels != floorf(pixels):
|
|
853
|
+
return {"ok": false, "error": "size is a whole number of pixels, and %s is not one." % str(pixels)}
|
|
854
|
+
if pixels < 1:
|
|
773
855
|
return {
|
|
774
856
|
"ok": false,
|
|
775
857
|
"error":
|
|
776
858
|
(
|
|
777
859
|
(
|
|
778
|
-
"size must be
|
|
779
|
-
|
|
860
|
+
"size must be 1 or more, not %d: the theme would hold it, and Godot reads a size of 0 or "
|
|
861
|
+
+ "less as unset and draws the default size instead."
|
|
780
862
|
)
|
|
781
|
-
|
|
863
|
+
% int(pixels)
|
|
782
864
|
)
|
|
783
865
|
}
|
|
784
866
|
theme.set_font_size(font_size_name, control_type, int(pixels))
|
|
@@ -819,4 +901,16 @@ func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
|
819
901
|
)
|
|
820
902
|
% [type, type]
|
|
821
903
|
)
|
|
904
|
+
# A variation is any name, so a misspelt class is written all the same, and the classes nearest
|
|
905
|
+
# it are what tells a caller which one was meant.
|
|
906
|
+
var themed: PackedStringArray = []
|
|
907
|
+
for known: String in ClassDB.get_class_list():
|
|
908
|
+
if ClassDB.is_parent_class(known, "Control") or ClassDB.is_parent_class(known, "Window"):
|
|
909
|
+
var _added: bool = themed.append(known)
|
|
910
|
+
var close: PackedStringArray = GdhRead.nearest(type, themed)
|
|
911
|
+
if not close.is_empty():
|
|
912
|
+
answer["note"] += (
|
|
913
|
+
" If a class was meant, the nearest %s %s."
|
|
914
|
+
% ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
915
|
+
)
|
|
822
916
|
return answer
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## What a new node in a scene is made or copied from, what the scene owns of it, and where among its
|
|
5
5
|
## siblings a node may go.
|
|
6
6
|
|
|
7
|
+
const GdhFileWalk = preload("../file_walk.gd")
|
|
7
8
|
const GdhRead = preload("../reading.gd")
|
|
8
9
|
const GdhPropertyValues = preload("../property_values.gd")
|
|
9
10
|
|
|
@@ -17,11 +18,15 @@ static func what_to_add(node_type: String, instance_path: String, scene_path: St
|
|
|
17
18
|
"add takes nodeType for a node of a class, or instancePath for a scene, and one of them only."
|
|
18
19
|
]
|
|
19
20
|
if not instance_path.is_empty():
|
|
21
|
+
# Asked before the cycle walk, which reads the file's dependencies and so loaded a file that
|
|
22
|
+
# was not there, laying the engine's error and a backtrace beneath the refusal.
|
|
23
|
+
if not ResourceLoader.exists(instance_path, "PackedScene"):
|
|
24
|
+
var said: String = "No scene at %s to instance" % instance_path
|
|
25
|
+
var offered: String = GdhFileWalk.new().with_nearest_files(said, instance_path)
|
|
26
|
+
return [null, offered if offered != said else said + "."]
|
|
20
27
|
var cycle: String = _instancing_cycle(scene_path, instance_path)
|
|
21
28
|
if not cycle.is_empty():
|
|
22
29
|
return [null, cycle]
|
|
23
|
-
if not ResourceLoader.exists(instance_path, "PackedScene"):
|
|
24
|
-
return [null, "No scene at %s to instance." % instance_path]
|
|
25
30
|
var packed: PackedScene = ResourceLoader.load(instance_path, "PackedScene") as PackedScene
|
|
26
31
|
if packed == null or not packed.can_instantiate():
|
|
27
32
|
return [null, "%s does not load as a scene that can be instanced." % instance_path]
|
|
@@ -35,12 +40,24 @@ static func what_to_add(node_type: String, instance_path: String, scene_path: St
|
|
|
35
40
|
)
|
|
36
41
|
]
|
|
37
42
|
if not ClassDB.class_exists(node_type) or not ClassDB.is_parent_class(node_type, "Node"):
|
|
38
|
-
return [null, "nodeType
|
|
43
|
+
return [null, not_a_node_class("nodeType", node_type)]
|
|
39
44
|
if not ClassDB.can_instantiate(node_type):
|
|
40
45
|
return [null, "%s cannot be instantiated." % node_type]
|
|
41
46
|
return [null, ""]
|
|
42
47
|
|
|
43
48
|
|
|
49
|
+
## The refusal for [param given], sent as [param argument], which names no node class, with the
|
|
50
|
+
## node classes nearest it, so "Node2DD" is offered Node2D.
|
|
51
|
+
static func not_a_node_class(argument: String, given: String) -> String:
|
|
52
|
+
var said: String = "%s must be a node class, and %s is not one" % [argument, given]
|
|
53
|
+
var classes: PackedStringArray = ClassDB.get_inheriters_from_class("Node")
|
|
54
|
+
var _added: bool = classes.append("Node")
|
|
55
|
+
var close: PackedStringArray = GdhRead.nearest(given, classes)
|
|
56
|
+
if close.is_empty():
|
|
57
|
+
return said + "."
|
|
58
|
+
return "%s; the nearest %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
59
|
+
|
|
60
|
+
|
|
44
61
|
## A new node of [param node_type], or an instance of [param packed] when there is one. The instance
|
|
45
62
|
## is made in the editor's edit state, so the pack writes it as an instance of its file, and owning
|
|
46
63
|
## its root leaves its own nodes to the instanced scene.
|
|
@@ -60,17 +60,15 @@ func _find_node(root: Node, path: String) -> Node:
|
|
|
60
60
|
## Why the property or signal [param missing] could not be found on [param node], adding the one
|
|
61
61
|
## cause the name alone does not show: a script that does not compile leaves the editor holding a
|
|
62
62
|
## placeholder with no properties or signals at all, and the refusal named the property instead.
|
|
63
|
+
## Such a script has no engine class beneath it. Compiling a copy of the source to find out failed on
|
|
64
|
+
## every script with a class_name, since the copy hides the global class the original declares.
|
|
63
65
|
func _missing_on(node: Node, missing: String) -> String:
|
|
64
66
|
var script: Script = node.get_script()
|
|
65
|
-
if script == null or not script.
|
|
66
|
-
return missing
|
|
67
|
-
var probe: GDScript = GDScript.new()
|
|
68
|
-
probe.source_code = script.source_code
|
|
69
|
-
if probe.reload() == OK:
|
|
67
|
+
if script == null or not str(script.get_instance_base_type()).is_empty():
|
|
70
68
|
return missing
|
|
71
69
|
return (
|
|
72
70
|
"%s. Its script, %s, does not compile, so none of what it declares is known here."
|
|
73
|
-
% [missing, script.resource_path]
|
|
71
|
+
% [missing.trim_suffix("."), script.resource_path]
|
|
74
72
|
)
|
|
75
73
|
|
|
76
74
|
|
|
@@ -204,9 +202,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
204
202
|
# A class that is not a node went into a Node variable, which is a runtime error the caller
|
|
205
203
|
# heard about only as "Invalid tool result".
|
|
206
204
|
if not ClassDB.class_exists(root_node_type) or not ClassDB.is_parent_class(root_node_type, "Node"):
|
|
207
|
-
return {
|
|
208
|
-
"ok": false, "error": "rootNodeType must be a node class, and %s is not one." % root_node_type
|
|
209
|
-
}
|
|
205
|
+
return {"ok": false, "error": GdhPlacing.not_a_node_class("rootNodeType", root_node_type)}
|
|
210
206
|
if not ClassDB.can_instantiate(root_node_type):
|
|
211
207
|
return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
|
|
212
208
|
var unfit: String = GdhSceneFile.unfit_name(root_node_name)
|
|
@@ -245,6 +241,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
245
241
|
# name is no property the write compares.
|
|
246
242
|
root.name = root_node_name
|
|
247
243
|
|
|
244
|
+
var had_folder: bool = DirAccess.dir_exists_absolute(scene_path.get_base_dir())
|
|
248
245
|
var folder: String = GdhSceneFile.make_folder_for(scene_path)
|
|
249
246
|
if not folder.is_empty():
|
|
250
247
|
root.free()
|
|
@@ -253,7 +250,12 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
|
|
|
253
250
|
if not written.is_empty():
|
|
254
251
|
return written
|
|
255
252
|
|
|
256
|
-
|
|
253
|
+
var answer: Dictionary = {
|
|
254
|
+
"ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name
|
|
255
|
+
}
|
|
256
|
+
if not had_folder:
|
|
257
|
+
answer["madeFolder"] = scene_path.get_base_dir()
|
|
258
|
+
return answer
|
|
257
259
|
|
|
258
260
|
|
|
259
261
|
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
@@ -343,7 +345,7 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
343
345
|
"nodePath": new_path,
|
|
344
346
|
"index": new_node.get_index(),
|
|
345
347
|
}
|
|
346
|
-
_properties.
|
|
348
|
+
_properties.note_write(answer, new_node)
|
|
347
349
|
var written: Dictionary = scene.write(
|
|
348
350
|
func(saved: Node) -> String:
|
|
349
351
|
return _kept(
|
|
@@ -608,7 +610,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
608
610
|
held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
|
|
609
611
|
var new_path: String = scene.path_of(node)
|
|
610
612
|
var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
|
|
611
|
-
_properties.
|
|
613
|
+
_properties.note_write(answer, node)
|
|
612
614
|
var carried: GdhPaths = GdhPaths.new()
|
|
613
615
|
var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
|
|
614
616
|
var written: Dictionary = scene.write(
|
|
@@ -848,7 +850,8 @@ func _connection_ends(
|
|
|
848
850
|
if not foreign.is_empty():
|
|
849
851
|
return [null, foreign]
|
|
850
852
|
if not source.has_signal(signal_name):
|
|
851
|
-
|
|
853
|
+
var said: String = "%s has no signal %s" % [source_node_path, signal_name]
|
|
854
|
+
return [null, _missing_on(source, GdhRead.offered(said, signal_name, GdhRead.signals_of(source)))]
|
|
852
855
|
return [source, target]
|
|
853
856
|
|
|
854
857
|
|
|
@@ -920,6 +923,7 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
920
923
|
|
|
921
924
|
return {
|
|
922
925
|
"ok": true,
|
|
926
|
+
"removed": true,
|
|
923
927
|
"sourceNodePath": from,
|
|
924
928
|
"signalName": signal_name,
|
|
925
929
|
"targetNodePath": to,
|
|
@@ -82,6 +82,13 @@ static func spare(fields: Dictionary, tag: String) -> Array:
|
|
|
82
82
|
##
|
|
83
83
|
## The refusal names only the two types, and the key is what the caller has to take out.
|
|
84
84
|
static func remedy(value: Variant, type: int) -> String:
|
|
85
|
+
if type == TYPE_STRING and typeof(value) in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]:
|
|
86
|
+
var number: Variant = value
|
|
87
|
+
if value is float:
|
|
88
|
+
var whole: float = value
|
|
89
|
+
if is_finite(whole) and whole == floorf(whole):
|
|
90
|
+
number = int(whole)
|
|
91
|
+
return ' Text is sent in quotes, as "%s".' % str(number)
|
|
85
92
|
var shape: String = written_as(type)
|
|
86
93
|
if shape.is_empty():
|
|
87
94
|
return ""
|
|
@@ -164,6 +164,43 @@ static func not_of_kind(
|
|
|
164
164
|
return told + " The scene's %ss are at %s." % [kind, shown]
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
## Up to three of [param names] closest to [param word], closest first, ignoring case: the names a
|
|
168
|
+
## refusal offers for one that is not there, as a typo usually is one of them.
|
|
169
|
+
static func nearest(word: String, names: PackedStringArray) -> PackedStringArray:
|
|
170
|
+
var wanted: String = word.to_lower()
|
|
171
|
+
var close: Array[String] = []
|
|
172
|
+
for name: String in names:
|
|
173
|
+
if name.to_lower().similarity(wanted) >= 0.5:
|
|
174
|
+
close.append(name)
|
|
175
|
+
close.sort_custom(
|
|
176
|
+
func(a: String, b: String) -> bool:
|
|
177
|
+
return a.to_lower().similarity(wanted) > b.to_lower().similarity(wanted)
|
|
178
|
+
)
|
|
179
|
+
return PackedStringArray(close.slice(0, 3))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## [param said], the refusal for [param word], which is not among [param names], with those nearest
|
|
183
|
+
## it when any are, or else every one of them, so the caller has a name to send instead.
|
|
184
|
+
static func offered(said: String, word: String, names: Array) -> String:
|
|
185
|
+
var every: PackedStringArray = PackedStringArray(names)
|
|
186
|
+
var close: PackedStringArray = nearest(word, every)
|
|
187
|
+
if close.is_empty():
|
|
188
|
+
return "%s; it has %s" % [said, ", ".join(every)]
|
|
189
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
193
|
+
## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
|
|
194
|
+
## starting with an underscore are the engine's own.
|
|
195
|
+
static func signals_of(object: Object) -> Array:
|
|
196
|
+
var names: Array = []
|
|
197
|
+
for entry: Dictionary in object.get_signal_list():
|
|
198
|
+
var name: String = str(entry["name"])
|
|
199
|
+
if not name.begins_with("_"):
|
|
200
|
+
names.push_front(name)
|
|
201
|
+
return names
|
|
202
|
+
|
|
203
|
+
|
|
167
204
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
168
205
|
static func with_article(word: String) -> String:
|
|
169
206
|
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
@@ -71,10 +71,36 @@ static func node_at(root: Node, path: String) -> Dictionary:
|
|
|
71
71
|
|
|
72
72
|
var node: Node = root.get_node_or_null(path)
|
|
73
73
|
if node == null:
|
|
74
|
-
return {"type": "error", "message":
|
|
74
|
+
return {"type": "error", "message": _not_found(root, path)}
|
|
75
75
|
return {"node": node}
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
## "Node not found" for [param path], with the deepest node of it the game has and the children that
|
|
79
|
+
## node has instead, as a scene's refusal names them: a mistyped step was otherwise found by reading
|
|
80
|
+
## the whole tree.
|
|
81
|
+
static func _not_found(root: Node, path: String) -> String:
|
|
82
|
+
var said: String = "Node not found: " + path
|
|
83
|
+
var parts: PackedStringArray = path.split("/", false)
|
|
84
|
+
var at: Node = root
|
|
85
|
+
var step: int = 0
|
|
86
|
+
if path.begins_with("/"):
|
|
87
|
+
if parts.is_empty() or parts[0] != str(root.name):
|
|
88
|
+
return said
|
|
89
|
+
step = 1
|
|
90
|
+
while step < parts.size():
|
|
91
|
+
var below: Node = at.get_node_or_null(NodePath(parts[step]))
|
|
92
|
+
if below == null:
|
|
93
|
+
break
|
|
94
|
+
at = below
|
|
95
|
+
step += 1
|
|
96
|
+
if step >= parts.size():
|
|
97
|
+
return said
|
|
98
|
+
var names: PackedStringArray = []
|
|
99
|
+
for child: Node in at.get_children():
|
|
100
|
+
var _named: bool = names.append(str(child.name))
|
|
101
|
+
return GdhRead.missing_below(said, "", PackedStringArray([str(at.get_path())]), parts[step], names)
|
|
102
|
+
|
|
103
|
+
|
|
78
104
|
## How many process frames a whole press holds before its release, from `holdFrames`: one unless
|
|
79
105
|
## asked, as the halves of a press have always been a frame apart. Held inside the game because the
|
|
80
106
|
## time between a press, a wait and a release sent as three calls counts as held too: a 60-frame
|
|
@@ -410,6 +410,10 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
410
410
|
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
411
411
|
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
412
412
|
return rebuilt
|
|
413
|
+
# A number or a truth value stays as sent where text is wanted, for the refusal to name. Converted,
|
|
414
|
+
# 1 arrived as the double it is in JSON and was saved as "1.0", which is not the text anyone sends.
|
|
415
|
+
if type == TYPE_STRING and not rebuilt is String:
|
|
416
|
+
return rebuilt
|
|
413
417
|
|
|
414
418
|
# Digits are read as digits, since read as JSON they become a double and one past 2^53 a
|
|
415
419
|
# neighbour of the number written.
|
|
@@ -82,6 +82,13 @@ static func spare(fields: Dictionary, tag: String) -> Array:
|
|
|
82
82
|
##
|
|
83
83
|
## The refusal names only the two types, and the key is what the caller has to take out.
|
|
84
84
|
static func remedy(value: Variant, type: int) -> String:
|
|
85
|
+
if type == TYPE_STRING and typeof(value) in [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]:
|
|
86
|
+
var number: Variant = value
|
|
87
|
+
if value is float:
|
|
88
|
+
var whole: float = value
|
|
89
|
+
if is_finite(whole) and whole == floorf(whole):
|
|
90
|
+
number = int(whole)
|
|
91
|
+
return ' Text is sent in quotes, as "%s".' % str(number)
|
|
85
92
|
var shape: String = written_as(type)
|
|
86
93
|
if shape.is_empty():
|
|
87
94
|
return ""
|
|
@@ -81,15 +81,7 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
|
|
|
81
81
|
listed.append(summary)
|
|
82
82
|
return {"ok": true, "animations": listed}
|
|
83
83
|
if not player.has_animation(StringName(animation_name)):
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
"ok": false,
|
|
87
|
-
"error":
|
|
88
|
-
(
|
|
89
|
-
"%s has no animation %s. It has: %s."
|
|
90
|
-
% [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
91
|
-
)
|
|
92
|
-
}
|
|
84
|
+
return {"ok": false, "error": no_animation(player, str(player.name), animation_name)}
|
|
93
85
|
var anim: Animation = player.get_animation(StringName(animation_name))
|
|
94
86
|
var answer: Dictionary = _summary(anim)
|
|
95
87
|
answer["ok"] = true
|
|
@@ -102,6 +94,16 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
|
|
|
102
94
|
return answer
|
|
103
95
|
|
|
104
96
|
|
|
97
|
+
## Why [param player], called [param called], cannot be asked for [param animation_name], with the
|
|
98
|
+
## animations it has.
|
|
99
|
+
static func no_animation(player: AnimationPlayer, called: String, animation_name: String) -> String:
|
|
100
|
+
var names: PackedStringArray = player.get_animation_list()
|
|
101
|
+
return (
|
|
102
|
+
"%s has no animation %s. It has: %s."
|
|
103
|
+
% [called, animation_name, ", ".join(names) if not names.is_empty() else "none"]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
105
107
|
static func _summary(anim: Animation) -> Dictionary:
|
|
106
108
|
return {
|
|
107
109
|
"length": anim.length,
|
|
@@ -183,13 +183,23 @@ func _not_found(wanted: String) -> Dictionary:
|
|
|
183
183
|
)
|
|
184
184
|
)
|
|
185
185
|
)
|
|
186
|
+
# The usual miss is a spelling, one letter off a class the engine has.
|
|
187
|
+
var close: PackedStringArray = GdhRead.nearest(wanted, ClassDB.get_class_list())
|
|
188
|
+
var offered: String = (
|
|
189
|
+
""
|
|
190
|
+
if close.is_empty()
|
|
191
|
+
else (
|
|
192
|
+
" The nearest engine %s %s."
|
|
193
|
+
% ["class is" if close.size() == 1 else "classes are", ", ".join(close)]
|
|
194
|
+
)
|
|
195
|
+
)
|
|
186
196
|
return _log.failure(
|
|
187
197
|
(
|
|
188
198
|
(
|
|
189
|
-
"Class not found: %s
|
|
190
|
-
+ "script declares is not in it either; script_info structure reads one."
|
|
199
|
+
"Class not found: %s.%s editor_classes reads the engine's ClassDB, so a class_name a "
|
|
200
|
+
+ "project script declares is not in it either; script_info structure reads one."
|
|
191
201
|
)
|
|
192
|
-
% wanted
|
|
202
|
+
% [wanted, offered]
|
|
193
203
|
)
|
|
194
204
|
)
|
|
195
205
|
|