[
  {
    "path": ".gitattributes",
    "content": ".gitattributes export-ignore\r\n.gitignore export-ignore\r\nLICENSE.txt export-ignore\r\nREADME.md export-ignore\r\nMedia export-ignore\r\nTests export-ignore\r\naddons/gut export-ignore\r\n"
  },
  {
    "path": ".gitignore",
    "content": ".godot/\r\n.export/\r\n.vscode/\r\n.editorconfig"
  },
  {
    "path": "Icons/Add.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://da1w5je87d6kc\"\npath=\"res://.godot/imported/Add.svg-4650b3c697b6839f9aaa41e5dae42c99.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Add.svg\"\ndest_files=[\"res://.godot/imported/Add.svg-4650b3c697b6839f9aaa41e5dae42c99.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/ArrowDown.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bidwkeg0fiqcn\"\npath=\"res://.godot/imported/ArrowDown.svg-26309e74d52d72e71bb12cc3f043a6a4.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/ArrowDown.svg\"\ndest_files=[\"res://.godot/imported/ArrowDown.svg-26309e74d52d72e71bb12cc3f043a6a4.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/ArrowLeft.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cp6em75230mi1\"\npath=\"res://.godot/imported/ArrowLeft.svg-8d8b9602b89315b7a20fcbae852b1ee0.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/ArrowLeft.svg\"\ndest_files=[\"res://.godot/imported/ArrowLeft.svg-8d8b9602b89315b7a20fcbae852b1ee0.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/ArrowRight.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bv4tqa6ixyi1h\"\npath=\"res://.godot/imported/ArrowRight.svg-94e81311cb12d9360d091b2f31b26fc1.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/ArrowRight.svg\"\ndest_files=[\"res://.godot/imported/ArrowRight.svg-94e81311cb12d9360d091b2f31b26fc1.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/ArrowUp.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://6orgtpcieuls\"\npath=\"res://.godot/imported/ArrowUp.svg-070bb575945f18f3f16743ecbc2b9556.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/ArrowUp.svg\"\ndest_files=[\"res://.godot/imported/ArrowUp.svg-070bb575945f18f3f16743ecbc2b9556.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Back.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dt6drd8cw1dhl\"\npath=\"res://.godot/imported/Back.svg-c3e09721b4490970ae5f7e525a9bc927.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Back.svg\"\ndest_files=[\"res://.godot/imported/Back.svg-c3e09721b4490970ae5f7e525a9bc927.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Copy.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dgcqwwuv1m48b\"\npath=\"res://.godot/imported/Copy.svg-e2e213b265521710a772cfe568c3515f.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Copy.svg\"\ndest_files=[\"res://.godot/imported/Copy.svg-e2e213b265521710a772cfe568c3515f.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Duplicate.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dnlqsyyx311xx\"\npath=\"res://.godot/imported/Duplicate.svg-fb11c0cbdffda6bd84053ae70f58784a.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Duplicate.svg\"\ndest_files=[\"res://.godot/imported/Duplicate.svg-fb11c0cbdffda6bd84053ae70f58784a.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Edit.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cfbagouqtgqfs\"\npath=\"res://.godot/imported/Edit.svg-bb367a6b4627125abbe892de75002153.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Edit.svg\"\ndest_files=[\"res://.godot/imported/Edit.svg-bb367a6b4627125abbe892de75002153.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Folder.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dhuukef617r6s\"\npath=\"res://.godot/imported/Folder.svg-7206efeac34d05a6803ca6e96330a38a.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Folder.svg\"\ndest_files=[\"res://.godot/imported/Folder.svg-7206efeac34d05a6803ca6e96330a38a.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Icon.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bwbaksun3rqh4\"\npath=\"res://.godot/imported/Icon.png-3c3ffee33c137a7bef3d4ae0dd705a04.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Icon.png\"\ndest_files=[\"res://.godot/imported/Icon.png-3c3ffee33c137a7bef3d4ae0dd705a04.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "Icons/Inherit.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://csjjbw7dnc51a\"\npath=\"res://.godot/imported/Inherit.svg-7dcf09ace19505a7a9e3f2bd1933fa98.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Inherit.svg\"\ndest_files=[\"res://.godot/imported/Inherit.svg-7dcf09ace19505a7a9e3f2bd1933fa98.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/MissingIcon.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dwlbcdps7ydkj\"\npath=\"res://.godot/imported/MissingIcon.svg-3d686d232f88a0b563b74f5451d11129.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/MissingIcon.svg\"\ndest_files=[\"res://.godot/imported/MissingIcon.svg-3d686d232f88a0b563b74f5451d11129.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Paste.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://3i2umr3kmry6\"\npath=\"res://.godot/imported/Paste.svg-c118cb57c4a613cc3b4fb177ad9ce2a5.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Paste.svg\"\ndest_files=[\"res://.godot/imported/Paste.svg-c118cb57c4a613cc3b4fb177ad9ce2a5.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Play.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bwtchwpfrbqsw\"\npath=\"res://.godot/imported/Play.svg-a90ebd04990ba96f2b12411054fcefec.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Play.svg\"\ndest_files=[\"res://.godot/imported/Play.svg-a90ebd04990ba96f2b12411054fcefec.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Remove.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://byn071xu1q5op\"\npath=\"res://.godot/imported/Remove.svg-fb94c466537cabd8d31d0dfa32623105.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Remove.svg\"\ndest_files=[\"res://.godot/imported/Remove.svg-fb94c466537cabd8d31d0dfa32623105.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "Icons/Script.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://ckqqfrho1pqcd\"\npath=\"res://.godot/imported/Script.svg-1bf269a1aea2aa7aa66daea0346cce3d.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Icons/Script.svg\"\ndest_files=[\"res://.godot/imported/Script.svg-1bf269a1aea2aa7aa66daea0346cce3d.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2024 Tomek\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Media/.gdignore",
    "content": ""
  },
  {
    "path": "Nodes/Command.gd",
    "content": "extends Control\n\n@onready var time: Label = %Time\n@onready var output_label: RichTextLabel = %OutputLabel\n\nvar task_text: String\nvar command: String\nvar arguments: PackedStringArray\nvar sensitive_strings: PackedStringArray\n\nvar raw_text: String\nvar error: String\n\nvar log_file: FileAccess\nvar program: ProgramInstance\n\nvar timer: float\nvar finish_code: int\n\nsignal success\nsignal fail\n\nfunc _ready() -> void:\n\t%TaskText.text = task_text\n\t\n\tlog_file.store_line(\"--- \" + task_text + \" ---\\n\")\n\t\n\tif not error.is_empty():\n\t\t%Status.text = \"Invalid\"\n\t\t%Status.modulate = Color.RED\n\t\t%Command.text = error\n\t\t%Command.modulate = Color.RED\n\t\t%Code.text = \"\"\n\t\t%Animation.queue_free()\n\t\tfail.emit()\n\t\tset_process(false)\n\t\treturn\n\t\n\traw_text = command + \" \" + \" \".join(arguments)\n\tif sensitive_strings.is_empty():\n\t\t%Command.text = raw_text\n\telse:\n\t\tvar command_text := raw_text\n\t\tfor string in sensitive_strings:\n\t\t\tcommand_text = command_text.replace(string, \"*\".repeat(string.length()))\n\t\t%Command.text = command_text\n\t\n\tvar pipe_data := OS.execute_with_pipe(command, arguments)\n\tprogram = ProgramInstance.create_for_pipe(pipe_data)\n\tprogram.output_line.connect(output_line)\n\tprogram.start()\n\nfunc output_line(line: String, is_error: bool):\n\tlog_file.store_line(line)\n\t\n\tif is_error:\n\t\toutput_label.push_color(Color.RED)\n\t\n\tif line.contains(\"\\u001b\"):\n\t\tline = line.\\\n\t\t\treplace(\"\\u001b[1m\", \"[b]\").\\\n\t\t\treplace(\"\\u001b[22m\", \"[/b]\").\\\n\t\t\treplace(\"\\u001b[90m\", \"[color=gray]\").\\\n\t\t\treplace(\"\\u001b[92m\", \"[color=green]\").\\\n\t\t\treplace(\"\\u001b[39m\", \"[/color]\").\\\n\t\t\treplace(\"\\u001b[0m\", \"\")\n\t\n\toutput_label.append_text(line)\n\t\n\tif is_error:\n\t\toutput_label.pop()\n\t\n\toutput_label.append_text(\"\\n\")\n\nfunc _process(delta: float) -> void:\n\tif program.is_running > 0:\n\t\ttimer += delta\n\t\tvar intime := int(timer)\n\t\ttime.text = \"%02d:%02d:%02d\" % [intime / 3600, intime / 60 % 60, intime % 60]\n\t\treturn\n\t\n\tprogram.finalize()\n\tset_process(false)\n\t\n\t%Animation.queue_free()\n\t\n\tif program.result == 0:\n\t\t%Status.text = \"Success\"\n\t\t%Status.modulate = Color.GREEN\n\t\t%Code.modulate = Color.GREEN\n\t\tsuccess.emit()\n\telse:\n\t\t%Status.text = \"Fail\"\n\t\t%Status.modulate = Color.RED\n\t\t%Code.modulate = Color.RED\n\t\tfail.emit()\n\t\n\t%Code.text = str(program.result)\n\tfinish_code = program.result\n\nfunc _on_command_gui_input(event: InputEvent) -> void:\n\tif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:\n\t\tDisplayServer.clipboard_set(raw_text)\n\t\t\n\t\tvar copied: Label = %Copied\n\t\tcopied.position = %Command.get_local_mouse_position()\n\t\tcopied.modulate.a = 1.0\n\t\tcopied.show()\n\t\t\n\t\tvar tween := copied.create_tween()\n\t\ttween.tween_property(copied, ^\"modulate:a\", 0.0, 0.5).set_delay(1)\n\t\ttween.tween_callback(copied.hide)\n\t\t\n\nfunc _exit_tree() -> void:\n\tif not program:\n\t\treturn\n\t\n\tif program.is_running > 0:\n\t\tprogram.stop()\n\tprogram.finalize()\n\nclass ProgramInstance:\n\tvar pid: int\n\tvar stdio: FileAccess\n\tvar stderr: FileAccess\n\tvar finish_mutext: Mutex\n\t\n\tvar is_running: int\n\tvar finalized: bool\n\tvar result: int\n\tvar io_thread: Thread\n\tvar err_thread: Thread\n\t\n\tsignal output_line(line: String, is_error: bool)\n\t\n\tstatic func create_for_pipe(data: Dictionary) -> ProgramInstance:\n\t\tvar instance := ProgramInstance.new()\n\t\tif data.is_empty():\n\t\t\treturn instance\n\t\t\n\t\tinstance.pid = data[\"pid\"]\n\t\tinstance.stdio = data[\"stdio\"]\n\t\tinstance.stderr = data[\"stderr\"]\n\t\tinstance.finish_mutext = Mutex.new()\n\t\tinstance.is_running = 2\n\t\t\n\t\treturn instance\n\t\n\tfunc start():\n\t\tio_thread = Thread.new()\n\t\tio_thread.start(pipe_read.bind(stdio))\n\t\t\n\t\terr_thread = Thread.new()\n\t\terr_thread.start(pipe_read.bind(stderr))\n\t\n\tfunc pipe_read(pipe: FileAccess):\n\t\tvar is_err := pipe == stderr\n\t\tvar buffer_empty: bool\n\t\t\n\t\twhile is_running > 0:\n\t\t\tif pipe.get_error() != OK or not OS.is_process_running(pid):\n\t\t\t\tbreak\n\t\t\t\n\t\t\tif buffer_empty:\n\t\t\t\toutput_line.emit.call_deferred(\"\", is_err)\n\t\t\tbuffer_empty = false\n\t\t\t\n\t\t\tvar line := pipe.get_line()\n\t\t\tif line.is_empty():\n\t\t\t\tbuffer_empty = true\n\t\t\telse:\n\t\t\t\toutput_line.emit.call_deferred(line, is_err)\n\t\t\n\t\tfinish_mutext.lock()\n\t\tis_running -= 1\n\t\tfinish_mutext.unlock()\n\t\n\tfunc stop():\n\t\tif is_running <= 0:\n\t\t\treturn\n\t\t\n\t\tis_running = 0\n\t\tOS.kill(pid)\n\t\tstdio.close()\n\t\tstderr.close()\n\t\n\tfunc finalize():\n\t\tif finalized:\n\t\t\treturn\n\t\t\n\t\tio_thread.wait_to_finish()\n\t\terr_thread.wait_to_finish()\n\t\tresult = OS.get_process_exit_code(pid)\n\t\tfinalized = true\n\nfunc toggle_output() -> void:\n\toutput_label.visible = not output_label.visible\n\nfunc copy_output() -> void:\n\tDisplayServer.clipboard_set(output_label.get_parsed_text())\n"
  },
  {
    "path": "Nodes/Command.gd.uid",
    "content": "uid://c5deg6k04uij2\n"
  },
  {
    "path": "Nodes/Command.tscn",
    "content": "[gd_scene load_steps=24 format=3 uid=\"uid://b6hrktoise2ji\"]\n\n[ext_resource type=\"Script\" uid=\"uid://c5deg6k04uij2\" path=\"res://Nodes/Command.gd\" id=\"1_l0kiu\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dno0spmwh4kov\" path=\"res://Nodes/Hourglass.png\" id=\"2_qna3k\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://csjjbw7dnc51a\" path=\"res://Icons/Inherit.svg\" id=\"3_kytcc\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dgcqwwuv1m48b\" path=\"res://Icons/Copy.svg\" id=\"4_0o8ph\"]\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_tas32\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(0, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_k5q5b\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(42, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_01srn\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(84, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_yxfrt\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(126, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_7bn30\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(168, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_kvoov\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(210, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_m48gl\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(252, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_1fsum\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(294, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_f1t7x\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(336, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_nbd80\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(378, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_1pyat\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(420, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_a8rgr\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(462, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_t6u08\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(504, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_blf8y\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(546, 0, 42, 42)\n\n[sub_resource type=\"AtlasTexture\" id=\"AtlasTexture_046av\"]\natlas = ExtResource(\"2_qna3k\")\nregion = Rect2(588, 0, 42, 42)\n\n[sub_resource type=\"SpriteFrames\" id=\"SpriteFrames_sf5fb\"]\nanimations = [{\n\"frames\": [{\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_tas32\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_k5q5b\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_01srn\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_yxfrt\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_7bn30\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_kvoov\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_m48gl\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_1fsum\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_f1t7x\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_nbd80\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_1pyat\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_a8rgr\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_t6u08\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_blf8y\")\n}, {\n\"duration\": 1.0,\n\"texture\": SubResource(\"AtlasTexture_046av\")\n}],\n\"loop\": true,\n\"name\": &\"default\",\n\"speed\": 5.0\n}]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_gis47\"]\ncontent_margin_left = 4.0\ncontent_margin_top = 4.0\ncontent_margin_right = 4.0\ncontent_margin_bottom = 4.0\nbg_color = Color(0, 0, 0, 1)\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_nmsdb\"]\nbg_color = Color(0, 0, 0, 0.12549)\n\n[sub_resource type=\"GDScript\" id=\"GDScript_jmu3s\"]\nscript/source = \"extends RichTextLabel\n\n@export var maximum_height: float\n\nfunc _ready() -> void:\n\tminimum_size_changed.connect(on_minimum_size_changed)\n\nfunc on_minimum_size_changed():\n\tif get_minimum_size().y > 300:\n\t\tcustom_minimum_size.y = 300\n\t\tfit_content = false\n\"\n\n[node name=\"Command\" type=\"PanelContainer\"]\noffset_right = 1000.0\noffset_bottom = 26.0\nsize_flags_horizontal = 3\nscript = ExtResource(\"1_l0kiu\")\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\".\"]\nlayout_mode = 2\ntheme_override_constants/margin_left = 8\ntheme_override_constants/margin_right = 8\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"MarginContainer\"]\ncustom_minimum_size = Vector2(1000, 0)\nlayout_mode = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MarginContainer/VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"VBoxContainer2\" type=\"VBoxContainer\" parent=\"MarginContainer/VBoxContainer/HBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"Status\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Executing Command\"\n\n[node name=\"Time\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"00:00:00\"\nhorizontal_alignment = 1\n\n[node name=\"Code\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(30, 0)\nlayout_mode = 2\nhorizontal_alignment = 1\n\n[node name=\"Control\" type=\"Control\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/Code\"]\nlayout_mode = 1\nanchors_preset = 8\nanchor_left = 0.5\nanchor_top = 0.5\nanchor_right = 0.5\nanchor_bottom = 0.5\ngrow_horizontal = 2\ngrow_vertical = 2\n\n[node name=\"Animation\" type=\"AnimatedSprite2D\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/Code/Control\"]\nunique_name_in_owner = true\nscale = Vector2(0.47619, 0.47619)\nsprite_frames = SubResource(\"SpriteFrames_sf5fb\")\nautoplay = \"default\"\nframe = 6\nframe_progress = 0.150927\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"MarginContainer/VBoxContainer/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"TaskText\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nmouse_filter = 0\ntext = \"Task\"\nhorizontal_alignment = 1\ntext_overrun_behavior = 3\n\n[node name=\"Command\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nmouse_filter = 0\ntext = \"What\"\nautowrap_mode = 2\n\n[node name=\"Copied\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Command\"]\nunique_name_in_owner = true\nvisible = false\nz_index = 1\nlayout_mode = 0\noffset_left = 24.0\noffset_top = 23.0\noffset_right = 183.0\noffset_bottom = 54.0\ntheme_override_colors/font_color = Color(1, 1, 0, 1)\ntheme_override_styles/normal = SubResource(\"StyleBoxFlat_gis47\")\ntext = \"Copied to clipboard\"\n\n[node name=\"HSeparator\" type=\"HSeparator\" parent=\"MarginContainer/VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"Output\" type=\"VBoxContainer\" parent=\"MarginContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MarginContainer/VBoxContainer/Output\"]\nlayout_mode = 2\nalignment = 1\n\n[node name=\"Button2\" type=\"Button\" parent=\"MarginContainer/VBoxContainer/Output/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Toggle Output\"\nicon = ExtResource(\"3_kytcc\")\n\n[node name=\"Button\" type=\"Button\" parent=\"MarginContainer/VBoxContainer/Output/HBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Copy Output\"\nicon = ExtResource(\"4_0o8ph\")\n\n[node name=\"OutputLabel\" type=\"RichTextLabel\" parent=\"MarginContainer/VBoxContainer/Output\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nfocus_mode = 2\ntheme_override_styles/normal = SubResource(\"StyleBoxFlat_nmsdb\")\nfit_content = true\nscroll_following = true\nautowrap_mode = 2\nthreaded = true\nprogress_bar_delay = -1\nselection_enabled = true\nscript = SubResource(\"GDScript_jmu3s\")\nmaximum_height = 600.0\n\n[connection signal=\"gui_input\" from=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/TaskText\" to=\".\" method=\"_on_command_gui_input\"]\n[connection signal=\"gui_input\" from=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Command\" to=\".\" method=\"_on_command_gui_input\"]\n[connection signal=\"pressed\" from=\"MarginContainer/VBoxContainer/Output/HBoxContainer/Button2\" to=\".\" method=\"toggle_output\"]\n[connection signal=\"pressed\" from=\"MarginContainer/VBoxContainer/Output/HBoxContainer/Button\" to=\".\" method=\"copy_output\"]\n"
  },
  {
    "path": "Nodes/GUI/DeleteButton.tscn",
    "content": "[gd_scene load_steps=4 format=3 uid=\"uid://m6a3ajkud85h\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://byn071xu1q5op\" path=\"res://Icons/Remove.svg\" id=\"1_xvgi5\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_v5hcl\"]\nscript/source = \"extends Button\n\n@onready var label: Label = $Label\n\nsignal confirmed\n\nvar timer: float\n\nfunc _ready() -> void:\n\tset_process(false)\n\nfunc _pressed() -> void:\n\tif label.visible:\n\t\tconfirmed.emit()\n\telse:\n\t\ttimer = 0.5\n\t\tlabel.show()\n\t\tset_process(true)\n\nfunc _process(delta: float) -> void:\n\ttimer -= delta\n\tif timer <= 0:\n\t\tlabel.hide()\n\t\tset_process(false)\n\"\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_liddr\"]\ncontent_margin_left = 2.0\ncontent_margin_right = 2.0\nbg_color = Color(0.145098, 0.156863, 0.188235, 1)\n\n[node name=\"DeleteButton\" type=\"Button\"]\nicon = ExtResource(\"1_xvgi5\")\nscript = SubResource(\"GDScript_v5hcl\")\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\nvisible = false\nz_index = 1\nlayout_mode = 1\nanchors_preset = 11\nanchor_left = 1.0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = 2.0\noffset_right = 174.0\ngrow_horizontal = 0\ngrow_vertical = 2\ntheme_override_styles/normal = SubResource(\"StyleBoxFlat_liddr\")\ntext = \"Click again to confirm\"\n"
  },
  {
    "path": "Nodes/GUI/DirectorySelector.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://cyl1d6reu3mk4\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://dhuukef617r6s\" path=\"res://Icons/Folder.svg\" id=\"1_34bwh\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_af0jw\"]\nscript/source = \"extends HBoxContainer\n\nenum Mode { SELECT_FOLDER, OPEN_FILE, SAVE_FILE, SELECT_WHATEVER, FAKE_SELECT_FOLDER }\nenum Scope { GLOBAL, PROJECT }\nenum MissingMode { IGNORE, WARN, ERROR }\n\n@onready var line_edit: LineEdit = $LineEdit\n@onready var file_dialog: FileDialog = $FileDialog\n\n@export var mode: Mode\n@export var scope: Scope\n@export var missing_mode: MissingMode\n@export var filters: PackedStringArray\n@export var empty_is_valid: bool\n\nsignal path_changed\n\nvar text: String:\n\tset(t):\n\t\tline_edit.text = t\n\t\tvalidate()\n\tget:\n\t\treturn line_edit.text\n\nfunc _ready() -> void:\n\tline_edit.text_changed.connect(path_changed.emit.unbind(1))\n\tmatch mode:\n\t\tMode.SELECT_FOLDER, Mode.FAKE_SELECT_FOLDER:\n\t\t\tfile_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR\n\t\tMode.OPEN_FILE:\n\t\t\tfile_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE\n\t\tMode.SAVE_FILE:\n\t\t\tfile_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE\n\t\tMode.SELECT_WHATEVER:\n\t\t\tfile_dialog.file_mode = FileDialog.FILE_MODE_OPEN_ANY\n\t\n\tif scope == Scope.PROJECT:\n\t\tfile_dialog.current_dir = Data.project_path\n\t\n\tfile_dialog.filters = filters\n\tvalidate()\n\nfunc open_dialog() -> void:\n\tfile_dialog.current_dir = text.get_base_dir()\n\tfile_dialog.popup_centered_ratio(0.5)\n\nfunc _path_selected(path: String) -> void:\n\tif scope == Scope.PROJECT:\n\t\ttext = path.trim_prefix(Data.project_path)\n\t\ttext = text.trim_prefix(\\\"/\\\")\n\telse:\n\t\ttext = path\n\t\n\tpath_changed.emit()\n\nfunc validate():\n\tif missing_mode == MissingMode.IGNORE:\n\t\treturn\n\t\n\tvar path := line_edit.text\n\tif empty_is_valid and path.is_empty():\n\t\tmodulate = Color.WHITE\n\t\treturn\n\t\n\tif scope == Scope.PROJECT:\n\t\tpath = Data.project_path.path_join(path)\n\t\n\tvar valid: bool\n\tif not line_edit.text.is_empty():\n\t\tmatch mode:\n\t\t\tMode.OPEN_FILE, Mode.SAVE_FILE, Mode.FAKE_SELECT_FOLDER:\n\t\t\t\tvalid = FileAccess.file_exists(path)\n\t\t\tMode.SELECT_FOLDER:\n\t\t\t\tvalid = DirAccess.dir_exists_absolute(path)\n\t\t\tMode.SELECT_WHATEVER:\n\t\t\t\tvalid = FileAccess.file_exists(path) or DirAccess.dir_exists_absolute(path)\n\t\n\tif valid:\n\t\tmodulate = Color.WHITE\n\t\treturn\n\t\n\tmatch missing_mode:\n\t\tMissingMode.WARN:\n\t\t\tmodulate = Color.YELLOW\n\t\tMissingMode.ERROR:\n\t\t\tmodulate = Color.RED\n\"\n\n[node name=\"DirectorySelector\" type=\"HBoxContainer\"]\noffset_right = 256.0\noffset_bottom = 31.0\nscript = SubResource(\"GDScript_af0jw\")\n\n[node name=\"LineEdit\" type=\"LineEdit\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Button\" type=\"Button\" parent=\".\"]\nlayout_mode = 2\nicon = ExtResource(\"1_34bwh\")\n\n[node name=\"FileDialog\" type=\"FileDialog\" parent=\".\"]\naccess = 2\n\n[connection signal=\"text_changed\" from=\"LineEdit\" to=\".\" method=\"validate\" unbinds=1]\n[connection signal=\"pressed\" from=\"Button\" to=\".\" method=\"open_dialog\"]\n[connection signal=\"dir_selected\" from=\"FileDialog\" to=\".\" method=\"_path_selected\"]\n[connection signal=\"file_selected\" from=\"FileDialog\" to=\".\" method=\"_path_selected\"]\n"
  },
  {
    "path": "Nodes/GUI/Disablabler.gd",
    "content": "extends Control\n\n@onready var initial_mouse := mouse_filter\n@onready var initial_focus := focus_mode\n\nfunc set_noexist(noexist: bool):\n\tmodulate.a = float(not noexist)\n\tprocess_mode = PROCESS_MODE_DISABLED if noexist else PROCESS_MODE_INHERIT\n\tmouse_filter = MOUSE_FILTER_IGNORE if noexist else initial_mouse\n\tfocus_mode = Control.FOCUS_NONE if noexist else initial_focus\n"
  },
  {
    "path": "Nodes/GUI/Disablabler.gd.uid",
    "content": "uid://dlv30xan3u6wy\n"
  },
  {
    "path": "Nodes/GUI/ExitShortcut.tres",
    "content": "[gd_resource type=\"Shortcut\" load_steps=2 format=3 uid=\"uid://d0olqx0bjlhp1\"]\n\n[sub_resource type=\"InputEventAction\" id=\"InputEventAction_i6wlr\"]\naction = &\"ui_cancel\"\n\n[resource]\nevents = [SubResource(\"InputEventAction_i6wlr\")]\n"
  },
  {
    "path": "Nodes/GUI/StringContainer.gd",
    "content": "extends ScrollContainer\n\n@onready var strings: HBoxContainer = %Strings\n\nvar string_prefab: PackedScene\n\nsignal changed\n\nfunc _ready() -> void:\n\tstring_prefab = Prefab.create(%StringPrefab)\n\nfunc _add_string() -> LineEdit:\n\tvar string: LineEdit = string_prefab.instantiate()\n\tstring.gui_input.connect(string_gui_input.bind(string))\n\tstring.text_changed.connect(emit_changed.unbind(1))\n\tstrings.add_child(string)\n\treturn string\n\nfunc string_gui_input(event: InputEvent, edit: LineEdit):\n\tif not edit.editable:\n\t\treturn\n\t\n\tif event is InputEventKey:\n\t\tif event.pressed and event.keycode == KEY_DELETE:\n\t\t\tedit.queue_free()\n\t\t\tedit.tree_exited.connect(emit_changed, CONNECT_DEFERRED)\n\nfunc get_strings() -> PackedStringArray:\n\treturn strings.get_children().map(func(line_edit: LineEdit) -> String: return line_edit.text)\n\nfunc set_strings(strins: PackedStringArray):\n\tfor s in strins:\n\t\tvar strin := _add_string()\n\t\tstrin.text = s\n\nfunc emit_changed():\n\tchanged.emit()\n"
  },
  {
    "path": "Nodes/GUI/StringContainer.gd.uid",
    "content": "uid://b640rv8k8mmlp\n"
  },
  {
    "path": "Nodes/GUI/StringContainer.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://bfbht01onlf1a\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://da1w5je87d6kc\" path=\"res://Icons/Add.svg\" id=\"1_nij3a\"]\n[ext_resource type=\"Script\" uid=\"uid://b640rv8k8mmlp\" path=\"res://Nodes/GUI/StringContainer.gd\" id=\"1_oa0rg\"]\n\n[node name=\"StringContainer\" type=\"ScrollContainer\"]\noffset_right = 187.0\noffset_bottom = 28.0\nhorizontal_scroll_mode = 2\nvertical_scroll_mode = 0\nscript = ExtResource(\"1_oa0rg\")\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Strings\" type=\"HBoxContainer\" parent=\"HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"StringPrefab\" type=\"LineEdit\" parent=\"HBoxContainer/Strings\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(100, 0)\nlayout_mode = 2\nexpand_to_text_length = true\n\n[node name=\"Button\" type=\"Button\" parent=\"HBoxContainer\"]\nlayout_mode = 2\nicon = ExtResource(\"1_nij3a\")\n\n[connection signal=\"pressed\" from=\"HBoxContainer/Button\" to=\".\" method=\"_add_string\"]\n[connection signal=\"pressed\" from=\"HBoxContainer/Button\" to=\".\" method=\"emit_changed\" flags=3]\n"
  },
  {
    "path": "Nodes/Hourglass.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dno0spmwh4kov\"\npath=\"res://.godot/imported/Hourglass.png-c86f4464194dba98591f5477463c57ed.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://Nodes/Hourglass.png\"\ndest_files=[\"res://.godot/imported/Hourglass.png-c86f4464194dba98591f5477463c57ed.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "Nodes/PresetTemplate.tscn",
    "content": "[gd_scene load_steps=8 format=3 uid=\"uid://bsgg4mwgvd5vi\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://bfbht01onlf1a\" path=\"res://Nodes/GUI/StringContainer.tscn\" id=\"1_egcl1\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"2_at7ma\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dnlqsyyx311xx\" path=\"res://Icons/Duplicate.svg\" id=\"3_yself\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://csjjbw7dnc51a\" path=\"res://Icons/Inherit.svg\" id=\"4_ricvi\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://m6a3ajkud85h\" path=\"res://Nodes/GUI/DeleteButton.tscn\" id=\"5_bn1pp\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_qhetb\"]\ncontent_margin_left = 8.0\ncontent_margin_top = 8.0\ncontent_margin_right = 8.0\ncontent_margin_bottom = 8.0\ndraw_center = false\nborder_width_left = 2\nborder_width_top = 2\nborder_width_right = 2\nborder_width_bottom = 2\nborder_color = Color(1, 1, 1, 0.501961)\n\n[sub_resource type=\"GDScript\" id=\"GDScript_4k674\"]\nscript/source = \"extends Control\n\nvar inherit: String\nvar parent: Node\n\nsignal changed\n\nfunc get_data() -> Dictionary:\n\tvar data: Dictionary\n\tdata[\\\"name\\\"] = %Name.text\n\tdata[\\\"custom_features\\\"] = %CustomFeatures.get_strings()\n\tdata[\\\"include_filters\\\"] = %IncludeFilters.get_strings()\n\tdata[\\\"exclude_filters\\\"] = %ExcludeFilters.get_strings()\n\tdata[\\\"export_path\\\"] = %ExportPath.text\n\tdata[\\\"inherit\\\"] = inherit\n\treturn data\n\nfunc set_data(data: Dictionary):\n\t%Name.text = data[\\\"name\\\"]\n\t%CustomFeatures.set_strings(data[\\\"custom_features\\\"])\n\t%IncludeFilters.set_strings(data[\\\"include_filters\\\"])\n\t%ExcludeFilters.set_strings(data[\\\"exclude_filters\\\"])\n\t%ExportPath.text = data[\\\"export_path\\\"]\n\t\n\tinherit = data.get(\\\"inherit\\\", \\\"\\\")\n\tupdate_inheritance()\n\tvalidate_name()\n\nfunc connect_duplicate(callback: Callable):\n\t%Duplicate.pressed.connect(callback)\n\nfunc connect_inherit(callback: Callable):\n\t%Inherit.pressed.connect(callback)\n\nfunc connect_delete(callback: Callable):\n\t%Delete.confirmed.connect(callback)\n\nfunc update_inheritance():\n\tif inherit.is_empty():\n\t\treturn\n\t\n\tif not parent:\n\t\tfor template in get_parent().get_children():\n\t\t\tif template.get_template_name() == inherit:\n\t\t\t\tparent = template\n\t\t\t\tparent.changed.connect(update_inheritance)\n\t\t\t\tbreak\n\t\n\tif parent.get_template_name() != inherit:\n\t\tinherit = parent.get_template_name()\n\t\n\tself_modulate = Color.YELLOW\n\t%Inherits.show()\n\t%Inherits.text = \\\"Inherits: %s\\\" % inherit\n\t\n\t$Timer.start()\n\nfunc sync_strings() -> void:\n\tvar other_template: Dictionary = parent.get_data()\n\tupdate_strings(%CustomFeatures, other_template[\\\"custom_features\\\"])\n\tupdate_strings(%IncludeFilters, other_template[\\\"include_filters\\\"])\n\tupdate_strings(%ExcludeFilters, other_template[\\\"exclude_filters\\\"])\n\nfunc update_strings(string_container: Control, strings: PackedStringArray):\n\tfor string: LineEdit in string_container.strings.get_children():\n\t\tif string.text in strings:\n\t\t\tstring.editable = false\n\t\telif not string.editable:\n\t\t\tstring.free()\n\t\n\tvar my_strings: PackedStringArray = string_container.get_strings()\n\tfor string in strings:\n\t\tif not string in my_strings:\n\t\t\tvar strin: LineEdit = string_container._add_string()\n\t\t\tstrin.text = string\n\t\t\tstrin.editable = false\n\nfunc get_template_name() -> String:\n\treturn %Name.text\n\nfunc validate_name() -> void:\n\tif get_parent().get_children().any(func(template: Node) -> bool: return template != self and template.get_template_name() == get_template_name()):\n\t\t%Name.modulate = Color.RED\n\telse:\n\t\t%Name.modulate = Color.WHITE\n\nfunc emit_changed():\n\tchanged.emit()\n\"\n\n[node name=\"PresetTemplate\" type=\"PanelContainer\"]\noffset_right = 771.0\noffset_bottom = 246.0\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_qhetb\")\nscript = SubResource(\"GDScript_4k674\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"GridContainer\" type=\"GridContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\ncolumns = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\ntext = \"Name\"\nhorizontal_alignment = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\n\n[node name=\"Name\" type=\"LineEdit\" parent=\"VBoxContainer/GridContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Inherits\" type=\"Label\" parent=\"VBoxContainer/GridContainer/HBoxContainer\"]\nunique_name_in_owner = true\nvisible = false\nlayout_mode = 2\ntheme_override_colors/font_color = Color(1, 1, 0, 1)\ntext = \"Inherits: %s\"\n\n[node name=\"Label2\" type=\"Label\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\ntext = \"Custom Features\"\nhorizontal_alignment = 2\n\n[node name=\"CustomFeatures\" parent=\"VBoxContainer/GridContainer\" instance=ExtResource(\"1_egcl1\")]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Label3\" type=\"Label\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\ntext = \"Include Filters\"\nhorizontal_alignment = 2\n\n[node name=\"IncludeFilters\" parent=\"VBoxContainer/GridContainer\" instance=ExtResource(\"1_egcl1\")]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Label4\" type=\"Label\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\ntext = \"Exclude Filters\"\nhorizontal_alignment = 2\n\n[node name=\"ExcludeFilters\" parent=\"VBoxContainer/GridContainer\" instance=ExtResource(\"1_egcl1\")]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Label5\" type=\"Label\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\ntext = \"Export Base\"\nhorizontal_alignment = 2\n\n[node name=\"ExportPath\" parent=\"VBoxContainer/GridContainer\" instance=ExtResource(\"2_at7ma\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nscope = 1\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"Duplicate\" type=\"Button\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 6\ntext = \"Duplicate\"\nicon = ExtResource(\"3_yself\")\nalignment = 0\n\n[node name=\"Inherit\" type=\"Button\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 6\ntext = \"Inherit\"\nicon = ExtResource(\"4_ricvi\")\nalignment = 0\n\n[node name=\"Delete\" parent=\"VBoxContainer/HBoxContainer\" instance=ExtResource(\"5_bn1pp\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 6\ntext = \"Delete\"\n\n[node name=\"Timer\" type=\"Timer\" parent=\".\"]\nwait_time = 0.5\none_shot = true\n\n[connection signal=\"text_changed\" from=\"VBoxContainer/GridContainer/HBoxContainer/Name\" to=\".\" method=\"validate_name\" unbinds=1]\n[connection signal=\"text_changed\" from=\"VBoxContainer/GridContainer/HBoxContainer/Name\" to=\".\" method=\"emit_changed\" unbinds=1]\n[connection signal=\"changed\" from=\"VBoxContainer/GridContainer/CustomFeatures\" to=\".\" method=\"emit_changed\"]\n[connection signal=\"changed\" from=\"VBoxContainer/GridContainer/IncludeFilters\" to=\".\" method=\"emit_changed\"]\n[connection signal=\"changed\" from=\"VBoxContainer/GridContainer/ExcludeFilters\" to=\".\" method=\"emit_changed\"]\n[connection signal=\"timeout\" from=\"Timer\" to=\".\" method=\"sync_strings\"]\n"
  },
  {
    "path": "Nodes/ProjectEntry.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://cc3kmk1iwkf5p\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_8e84h\"]\nscript/source = \"extends MarginContainer\n\nfunc set_project(path: String, callback: Callable):\n\t%Path.text = path\n\t\n\tvar config := ConfigFile.new()\n\tconfig.load(path.path_join(\\\"project.godot\\\"))\n\t\n\t%Name.text = config.get_value(\\\"application\\\", \\\"config/name\\\", \\\"[unnamed]\\\")\n\t\n\tvar icon_path: String = config.get_value(\\\"application\\\", \\\"config/icon\\\", \\\"\\\")\n\tvar icon := get_icon(icon_path, path)\n\t\n\tif not icon:\n\t\ticon = preload(\\\"uid://dwlbcdps7ydkj\\\")\n\t%Icon.texture = icon\n\t\n\tvar local_config_file := Data.get_project_config_path(path, config)\n\tvar task := FileAccess.open(path.path_join(local_config_file), FileAccess.READ)\n\tif not task:\n\t\t%Tasks.text = \\\"Project builder not configured\\\"\n\telse:\n\t\tvar data: Dictionary = str_to_var(task.get_as_text())\n\t\t%Tasks.text = \\\"%d routines\\\" % data.get(\\\"routines\\\", []).size()\n\t\n\t$Button.pressed.connect(callback.bind(path))\n\nfunc get_icon(path: String, project_path: String) -> Texture2D:\n\tif path.is_empty():\n\t\treturn null\n\t\n\tif path.begins_with(\\\"uid://\\\"):\n\t\tvar uid_cache := FileAccess.open(project_path.path_join(\\\".godot/uid_cache.bin\\\"), FileAccess.READ)\n\t\tif not uid_cache:\n\t\t\treturn null\n\t\t\n\t\tvar entry_count := uid_cache.get_32()\n\t\tfor i in entry_count:\n\t\t\tvar id := uid_cache.get_64()\n\t\t\tvar length := uid_cache.get_32()\n\t\t\t\n\t\t\tvar buffer := uid_cache.get_buffer(length)\n\t\t\tif ResourceUID.id_to_text(id) == path:\n\t\t\t\tpath = buffer.get_string_from_ascii()\n\t\t\t\tbreak\n\t\n\tpath = path.replace(\\\"res:/\\\", project_path)\n\tif FileAccess.file_exists(path):\n\t\tvar image := Image.load_from_file(path)\n\t\tif image:\n\t\t\treturn ImageTexture.create_from_image(image)\n\t\n\treturn null\n\"\n\n[node name=\"ProjectEntry\" type=\"MarginContainer\"]\ncustom_minimum_size = Vector2(800, 0)\noffset_right = 495.0\noffset_bottom = 113.0\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nscript = SubResource(\"GDScript_8e84h\")\n\n[node name=\"Button\" type=\"Button\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\".\"]\nlayout_mode = 2\nmouse_filter = 2\ntheme_override_constants/margin_left = 8\ntheme_override_constants/margin_top = 8\ntheme_override_constants/margin_right = 8\ntheme_override_constants/margin_bottom = 8\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"MarginContainer\"]\nlayout_mode = 2\nmouse_filter = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MarginContainer/VBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nmouse_filter = 2\n\n[node name=\"Icon\" type=\"TextureRect\" parent=\"MarginContainer/VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(128, 128)\nlayout_mode = 2\nmouse_filter = 2\nexpand_mode = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"MarginContainer/VBoxContainer/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nmouse_filter = 2\n\n[node name=\"Name\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_vertical = 3\ntheme_override_font_sizes/font_size = 40\ntext = \"Project Name\"\nhorizontal_alignment = 1\nvertical_alignment = 1\n\n[node name=\"Tasks\" type=\"Label\" parent=\"MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_vertical = 3\ntext = \"Tasks\"\nhorizontal_alignment = 1\nvertical_alignment = 1\n\n[node name=\"Path\" type=\"Label\" parent=\"MarginContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"Path\"\n"
  },
  {
    "path": "Nodes/RoutinePreview.tscn",
    "content": "[gd_scene load_steps=9 format=3 uid=\"uid://cctfsldanqcig\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://dnlqsyyx311xx\" path=\"res://Icons/Duplicate.svg\" id=\"1_1u4fl\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cp6em75230mi1\" path=\"res://Icons/ArrowLeft.svg\" id=\"1_17425\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://bwtchwpfrbqsw\" path=\"res://Icons/Play.svg\" id=\"1_hqapv\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cfbagouqtgqfs\" path=\"res://Icons/Edit.svg\" id=\"1_m46hf\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://bv4tqa6ixyi1h\" path=\"res://Icons/ArrowRight.svg\" id=\"2_0d0yc\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://m6a3ajkud85h\" path=\"res://Nodes/GUI/DeleteButton.tscn\" id=\"6_hh3q2\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_e2baq\"]\ncontent_margin_left = 8.0\ncontent_margin_top = 8.0\ncontent_margin_right = 8.0\ncontent_margin_bottom = 8.0\ndraw_center = false\nborder_width_left = 2\nborder_width_top = 2\nborder_width_right = 2\nborder_width_bottom = 2\nborder_color = Color(1, 1, 1, 0.501961)\n\n[sub_resource type=\"GDScript\" id=\"GDScript_s214c\"]\nscript/source = \"extends Control\n\n@onready var move_left: Button = %MoveLeft\n@onready var move_right: Button = %MoveRight\n\nvar data: Dictionary\n\nfunc _ready() -> void:\n\tupdate_buttons.call_deferred()\n\nfunc set_routine_data(d: Dictionary):\n\tdata = d\n\t%Name.text = data[\\\"name\\\"]\n\tif data[\\\"tasks\\\"].is_empty():\n\t\t%TaskCount.text = \\\"No Tasks\\\"\n\t\t%Execute.disabled = true\n\t\treturn\n\t\n\t%TaskCount.text %= data[\\\"tasks\\\"].size()\n\t\n\tvar task_list: PackedStringArray\n\tfor task in data[\\\"tasks\\\"]:\n\t\tvar task_info: Dictionary = Data.tasks[task[\\\"scene\\\"]]\n\t\ttask_list.append(task_info[\\\"name\\\"])\n\t\n\t%TaskList.text = \\\"\\\\n\\\".join(task_list)\n\nfunc connect_execute(target: Callable):\n\t%Execute.pressed.connect(target)\n\nfunc connect_edit(target: Callable):\n\t%Edit.pressed.connect(target)\n\nfunc connect_duplicate(target: Callable):\n\t%Duplicate.pressed.connect(target)\n\nfunc delete() -> void:\n\tData.routines.erase(data)\n\tData.queue_save_local_config()\n\tqueue_free()\n\nfunc update_buttons():\n\tmove_left.disabled = get_index() == 0\n\tmove_right.disabled = get_index() == get_parent().get_child_count() - 1\n\nfunc refresh_parent():\n\towner.sync_routines()\n\tData.queue_save_local_config()\n\t\n\tfor child in get_parent().get_children():\n\t\tchild.update_buttons()\n\nfunc _on_move_left_pressed() -> void:\n\tget_parent().move_child(self, get_index() - 1)\n\trefresh_parent()\n\nfunc _on_move_right_pressed() -> void:\n\tget_parent().move_child(self, get_index() + 1)\n\trefresh_parent()\n\"\n\n[node name=\"Routinepreview\" type=\"PanelContainer\"]\ncustom_minimum_size = Vector2(400, 0)\noffset_right = 303.0\noffset_bottom = 203.0\nsize_flags_horizontal = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_e2baq\")\nscript = SubResource(\"GDScript_s214c\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"MoveLeft\" type=\"Button\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nicon = ExtResource(\"1_17425\")\n\n[node name=\"Name\" type=\"Label\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\ntheme_override_font_sizes/font_size = 20\ntext = \"Routine Name\"\nhorizontal_alignment = 1\n\n[node name=\"MoveRight\" type=\"Button\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nicon = ExtResource(\"2_0d0yc\")\n\n[node name=\"TaskCount\" type=\"Label\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"%d tasks\"\nhorizontal_alignment = 1\n\n[node name=\"TaskList\" type=\"Label\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nmodulate = Color(0.8, 0.8, 0.8, 1)\nlayout_mode = 2\nsize_flags_vertical = 3\nhorizontal_alignment = 1\n\n[node name=\"GridContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"Execute\" type=\"Button\" parent=\"VBoxContainer/GridContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"Execute\"\nicon = ExtResource(\"1_hqapv\")\nalignment = 0\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/GridContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Edit\" type=\"Button\" parent=\"VBoxContainer/GridContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Edit\"\nicon = ExtResource(\"1_m46hf\")\nalignment = 0\n\n[node name=\"Duplicate\" type=\"Button\" parent=\"VBoxContainer/GridContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Duplicate\"\nicon = ExtResource(\"1_1u4fl\")\nalignment = 0\n\n[node name=\"DeleteButton\" parent=\"VBoxContainer\" instance=ExtResource(\"6_hh3q2\")]\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Delete\"\n\n[connection signal=\"pressed\" from=\"VBoxContainer/HBoxContainer/MoveLeft\" to=\".\" method=\"_on_move_left_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/HBoxContainer/MoveRight\" to=\".\" method=\"_on_move_right_pressed\"]\n[connection signal=\"confirmed\" from=\"VBoxContainer/DeleteButton\" to=\".\" method=\"delete\"]\n"
  },
  {
    "path": "Nodes/Task.gd",
    "content": "extends Control\nclass_name Task\n\n## If [code]true[/code], the script will receive [method _initialize_project] and [method _process_file] calls when a project is opened.\n@export var has_static_configuration: bool\n\n## Set [code]true[/code] when the task is using any sensitive setting, like passwords.\n@export var has_sensitive_data: bool\n\n## Default values for data properties. Set them inside [method _initialize].\nvar defaults: Dictionary#[String, Variant]\n## The actual data values. Use [method _load] and [method _store] for serializing it.\nvar data: Dictionary#[String, Variant]\n\n## Set this for displaying error message in [method _prevalidate] and [method _validate].\nvar error_message: String\n\n## The name that displays on the list of tasks. It should be a constant string.\nfunc _get_task_name() -> String:\n\treturn \"Empty Task\"\n\n## The name that displays when the task is being executed. You can provide extra details about the configuration from [member data].\nfunc _get_execute_string() -> String:\n\treturn _get_task_name()\n\n## Called when a project is loaded if [member has_static_configuration] is [code]true[/code]. You can use it to initialize static data specific to a project, which should be global to each instance of this task. If you have a cache file created in [method _end_project_scan], it should be loaded here.\nstatic func _initialize_project() -> void:\n\tpass\n\n## Called when a project is loaded for the first time or when the user starts rescan, if [member has_static_configuration] is [code]true[/code]. You can use it to clear file cache if you have one.\nstatic func _begin_project_scan() -> void:\n\tpass\n\n## Called after [method _begin_project_scan] if [member has_static_configuration] is [code]true[/code]. This method will be invoked for every file in the project. You can use it to collect configuration files specific for this task. Since this method is not called on every project load, you need to cache the result.\nstatic func _process_file(path: String) -> void:\n\tpass\n\n## Called when project scanning is finished. Here you can cache the result of scan using [method get_cache_file].\nstatic func _end_project_scan() -> void:\n\tpass\n\n## Called when the task is being initialized. Use it to fill [member defaults] and initialize child [Control] node values.\nfunc _initialize() -> void:\n\tpass\n\n## Called at the beginning of a routine to check for obvious mistakes and missing information. Return [code]false[/code] if the task can't run and fill [member error_message].\nfunc _prevalidate() -> bool:\n\treturn true\n\n## Called just before task is executed to check for invalid configuration, especially when it depends on previous tasks. Return [code]false[/code] if the task can't run and fill [member error_message].\nfunc _validate() -> bool:\n\treturn true\n\n## Return the executable name that will run this task.\nfunc _get_command() -> String:\n\treturn \"\"\n\n## Return the arguments provided for launching the executable.\nfunc _get_arguments() -> PackedStringArray:\n\treturn []\n\n## Called before running the task. Use it to setup necessary configuration for running the task, especially temporary one.\nfunc _prepare() -> void:\n\tpass\n\n## Called after the task has finished or when aborting execution. Use it to cleanup temporary configuration created in [method _prepare].\nfunc _cleanup() -> void:\n\tpass\n\n## Called when the task data is being loaded. Use [member data] to retrieve data and push it to child [Control] nodes.\nfunc _load() -> void:\n\tpass\n\n## Called when the task data is being saved. Store any persistent information in the [member data] [Dictionary]. Any missing property will be filled from [member defaults].\nfunc _store() -> void:\n\tpass\n\n## Return the description of this task and its parameters.\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\"Task description\",\n\t\t\"Argument Name|Description\",\n\t]\n\nfunc load_data(new_data: Dictionary) -> void:\n\tdata = new_data.merged(defaults)\n\t_load()\n\nfunc store_data() -> Dictionary:\n\t_store()\n\treturn data.merged(defaults)\n\nstatic func create_instance(scene: String) -> Task:\n\treturn Data.tasks[scene][\"scene_cache\"].instantiate()\n\n## Returns a [FileAccess] instance for the specified file and flags or [code]null[/code] if file can't be created/loaded. [param file] is name of the file, which will be located in a cache directory.\nstatic func get_cache_file(file: String, flags: FileAccess.ModeFlags) -> FileAccess:\n\tDirAccess.make_dir_absolute(\"user://FileCache\")\n\treturn FileAccess.open(\"user://FileCache\".path_join(str(Data.project_path.get_file(), \" - \", file)), flags)\n"
  },
  {
    "path": "Nodes/Task.gd.uid",
    "content": "uid://qg3dm7um8fj4\n"
  },
  {
    "path": "Nodes/TaskContainer.tscn",
    "content": "[gd_scene load_steps=7 format=3 uid=\"uid://fktnevmh7mia\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://bwtchwpfrbqsw\" path=\"res://Icons/Play.svg\" id=\"1_0viqh\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://6orgtpcieuls\" path=\"res://Icons/ArrowUp.svg\" id=\"1_efkxy\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://bidwkeg0fiqcn\" path=\"res://Icons/ArrowDown.svg\" id=\"2_r8s42\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dgcqwwuv1m48b\" path=\"res://Icons/Copy.svg\" id=\"4_fcytw\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://m6a3ajkud85h\" path=\"res://Nodes/GUI/DeleteButton.tscn\" id=\"5_ewh0j\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_f3a8a\"]\nscript/source = \"extends PanelContainer\n\nvar task: Task\n\nsignal copied\n\nfunc _ready() -> void:\n\tupdate_buttons.call_deferred()\n\nfunc set_task_scene(scene: String) -> Task:\n\ttask = Task.create_instance(scene)\n\t%TaskName.text = task._get_task_name()\n\t%TaskHere.add_child(task)\n\treturn task\n\nfunc update_buttons():\n\t%Up.disabled = get_index() == 0\n\t%Down.disabled = get_index() == get_parent().get_child_count() - 1\n\nfunc move_up() -> void:\n\tget_parent().move_child(self, get_index() - 1)\n\tupdate_buttons()\n\nfunc move_down() -> void:\n\tget_parent().move_child(self, get_index() + 1)\n\tupdate_buttons()\n\nfunc erase() -> void:\n\tqueue_free()\n\nfunc copy():\n\tvar task_data := Dictionary()\n\ttask_data[\\\"scene\\\"] = task.scene_file_path.get_file().get_basename()\n\ttask_data[\\\"data\\\"] = task.store_data()\n\tData.copied_task = task_data\n\tcopied.emit()\n\nfunc test_task() -> void:\n\towner.test_task(task)\n\"\n\n[node name=\"TaskContainer\" type=\"PanelContainer\"]\noffset_right = 587.0\noffset_bottom = 48.0\nscript = SubResource(\"GDScript_f3a8a\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\ntheme_override_constants/margin_left = 4\ntheme_override_constants/margin_top = 4\ntheme_override_constants/margin_right = 4\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer/MarginContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 0\ntooltip_text = \"Execute This Task\"\nicon = ExtResource(\"1_0viqh\")\n\n[node name=\"TaskName\" type=\"Label\" parent=\"VBoxContainer/MarginContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"Task Name\"\nhorizontal_alignment = 1\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer/MarginContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 8\n\n[node name=\"Up\" type=\"Button\" parent=\"VBoxContainer/MarginContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntooltip_text = \"Move Up\"\nicon = ExtResource(\"1_efkxy\")\n\n[node name=\"Down\" type=\"Button\" parent=\"VBoxContainer/MarginContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntooltip_text = \"Move Down\"\nicon = ExtResource(\"2_r8s42\")\n\n[node name=\"Button3\" type=\"Button\" parent=\"VBoxContainer/MarginContainer/HBoxContainer\"]\nlayout_mode = 2\ntooltip_text = \"Copy\"\nicon = ExtResource(\"4_fcytw\")\n\n[node name=\"VSeparator\" type=\"VSeparator\" parent=\"VBoxContainer/MarginContainer/HBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"DeleteButton\" parent=\"VBoxContainer/MarginContainer/HBoxContainer\" instance=ExtResource(\"5_ewh0j\")]\nlayout_mode = 2\n\n[node name=\"PanelContainer\" type=\"PanelContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"TaskHere\" type=\"MarginContainer\" parent=\"VBoxContainer/PanelContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntheme_override_constants/margin_left = 10\ntheme_override_constants/margin_top = 10\ntheme_override_constants/margin_right = 10\ntheme_override_constants/margin_bottom = 10\n\n[connection signal=\"pressed\" from=\"VBoxContainer/MarginContainer/Button\" to=\".\" method=\"test_task\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/MarginContainer/HBoxContainer/Up\" to=\".\" method=\"move_up\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/MarginContainer/HBoxContainer/Down\" to=\".\" method=\"move_down\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/MarginContainer/HBoxContainer/Button3\" to=\".\" method=\"copy\"]\n[connection signal=\"confirmed\" from=\"VBoxContainer/MarginContainer/HBoxContainer/DeleteButton\" to=\".\" method=\"erase\"]\n"
  },
  {
    "path": "Nodes/TaskPreview.tscn",
    "content": "[gd_scene load_steps=4 format=3 uid=\"uid://dv8u0se3f7m7s\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_75d6r\"]\ncontent_margin_left = 8.0\ncontent_margin_top = 8.0\ncontent_margin_right = 8.0\ncontent_margin_bottom = 8.0\ndraw_center = false\nborder_width_left = 2\nborder_width_top = 2\nborder_width_right = 2\nborder_width_bottom = 2\nborder_color = Color(1, 1, 1, 0.501961)\n\n[sub_resource type=\"GDScript\" id=\"GDScript_d6iij\"]\nscript/source = \"extends Control\n\n@onready var task_description: RichTextLabel = %TaskDescription\n\nvar task: Dictionary\n\nfunc _ready() -> void:\n\tvar task_instance := Task.create_instance(task[\\\"scene\\\"])\n\t%TaskContainer.add_child(task_instance)\n\t\n\t%TaskName.text = task_instance._get_task_name()\n\t\n\tvar info := task_instance._get_task_info()\n\ttask_description.append_text(info[0])\n\t\n\tif info.size() < 2:\n\t\treturn\n\t\n\ttask_description.append_text(\\\"\\\\n\\\\n[center]Parameters[/center]\\\")\n\t\n\tfor i in range(1, info.size()):\n\t\tvar argument := info[i]\n\t\ttask_description.append_text(\\\"\\\\n[b]%s:[/b] %s\\\" % [argument.get_slice(\\\"|\\\", 0), argument.get_slice(\\\"|\\\", 1)])\n\"\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_6p44c\"]\ncontent_margin_left = 4.0\ncontent_margin_top = 4.0\ncontent_margin_right = 4.0\ncontent_margin_bottom = 4.0\nbg_color = Color(0, 0, 0, 0.25098)\n\n[node name=\"TaskPreview\" type=\"PanelContainer\"]\noffset_right = 344.0\noffset_bottom = 221.0\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_75d6r\")\nscript = SubResource(\"GDScript_d6iij\")\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 2\ntheme_override_constants/separation = 20\n\n[node name=\"TaskContainer\" type=\"PanelContainer\" parent=\"HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 4\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_6p44c\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"TaskName\" type=\"Label\" parent=\"HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntheme_override_font_sizes/font_size = 20\ntext = \"Task Name\"\nhorizontal_alignment = 1\n\n[node name=\"TaskDescription\" type=\"RichTextLabel\" parent=\"HBoxContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_vertical = 3\nbbcode_enabled = true\nfit_content = true\nscroll_active = false\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"Icons/Icon.png\" width=\"64\" height=\"64\"> Godot Project Builder\r\n\r\nProject Builder is an automation tool made in Godot Engine and focused on exporting and publishing Godot projects. The builder works by running \"routines\", which are composed of \"tasks\". Each task involves running a predefined command with customized arguments. The builder comes with numberous built-in tasks, which include exporting project, publishing to popular stores (Steam, GOG, Epic, itch.io), and file operations. Task setup is fully visual.\r\n\r\nRunning the tool from source requires at least Godot 4.3. You can pick an executable from the official releases. Project Builder can be used with projects from older Godot versions, including Godot 3.\r\n\r\n## Overview\r\n\r\nProject Builder can be launched from a release executable or from source, using Godot Engine. When you start Project Builder, the first thing you will see is the project list.\r\n\r\n![](Media/ProjectList.png)\r\n\r\nIt's similar to Godot's own Project Manager, but more minimal. It automatically lists the projects you have registered in Godot, with information whether Project Builder is configured for that project and how many routines it has. You can provide a custom project list file (in case you run in self-contained mode or just want the list to be better organized), by setting it in the text field at the top.\r\n\r\nClicking a project will open the main project view.\r\n\r\n![](Media/MainRoutines.png)\r\n\r\nIt shows projects title at the top and is divided into 4 tabs: Routines, Templates, Tasks and Config. They are explained below.\r\n\r\n### Routines\r\n\r\nThis tab shows overview of all routines in your project. You can create a routine by clicking Add Routine button, which will add an empty one:\r\n\r\n![](Media/EmptyRoutine.png)\r\n\r\nFrom the main view, routines can be executed, edited, duplicated or deleted. When you create and edit a routine, it will be automatically saved in a dedicated project build configuration file stored in your project's folder. By default the file is located at `res://project_builds_config.txt`. When a routine has tasks, it will show a brief list.\r\n\r\n![](Media/RoutineExample.png)\r\n\r\n#### Editing Routines\r\n\r\nBy clicking the Edit button in a routine, you will open an editing screen where you can assign tasks to a routine.\r\n\r\n![](Media/RoutineEditing.png)\r\n\r\nOn this screen you'll see a list of all tasks assigned to the routine. You can add a task by clicking Add Task button and selecting a task from the list.\r\n\r\n![](Media/RoutineTaskList.png)\r\n\r\nAfter the task is added, you can use its controls to setup it. Reference about task usages and its configuration is available in the [Tasks](#tasks) tab or in the [task list](#list-of-available-tasks) in this README.\r\n\r\nAt the top of the task preview is its title and various buttons. The top-left button allows you to quickly execute a single task from the routine (see [Executing](#executing)). The top-right has buttons for rearranging tasks, a Copy button and a Delete button. Copy will copy the task data into an internal clipboard and you will be able to paste the task into the same routine, other routines, or even other projects. The Delete button requires double-click to activate, to prevent accidental deleting. A deleted task cannot be recovered, as currently the Project Builder does not support undo/redo.\r\n\r\nAbove the task list is the routine name field, where you can rename it. Note that each routine needs an unique name and if there is a conflict, you won't be able to leave this screen (unless you use the Discard Changes button). Aside from name, there is a dropdown for controlling what happens when a task fails (see [Executing](#executing)). Clicking Back will go back to the main screen and save the routine. The routine is also saved when you close the Project Builder.\r\n\r\n#### Executing\r\n\r\nBy clicking the Execute button in a routine or using using the quick-execute button in routine editing screen, you will enter the execution mode. If you did this by accident, you have 2 seconds (default) to press Escape and cancel task (afterwards you have to close the application if you want to stop it). The initial delay can be configured in Global Config.\r\n\r\n![](Media/RoutineExecuting.gif)\r\n\r\nExecution works simply by running tasks one after another. A task will run a command with arguments and display the output in real time. Once a task finishes, you will see whether it succeeded or failed, its exit code, and the time it took to execute.\r\n\r\n![](Media/FinishedTask.png)\r\n\r\nTop of the task is its execution name (i.e. a more detailed task name) and the exact command that is executed when the task runs (in the example above, the task invoked Godot with `--export-release`). Clicking that command will copy it to the clipboard (so you can e.g. paste it into terminal, edit and run manually). Below is Toggle Output button that allows you to hide output and Copy Output, which will copy it to the clipboard. Output is also written to log files that can be accessed from Open Log Folder on the project's main screen.\r\n\r\nNormally when a task fails, the whole execution will stop. You can configure that in routine settings; the other option is to continue executing normally. When a routine finishes, you will see the total time it took to execute.\r\n\r\n![](Media/FinishedRoutine.png)\r\n\r\nThe routine may also fail before it starts running. This can happen when some tasks have prevalidation step, i.e. they check prematurely if their configuration is invalid and guaranteed to fail the task. E.g. Upload Steam task will fail if you didn't provide Steam CMD path.\r\n\r\n![](Media/FailExample.png)\r\n\r\n### Preset Templates\r\n\r\nThe second tab in the main screen. It allows defining templates for export presets. It's a way to share properties like file filters or feature tags between mutliple presets, making multiple export targets easier to manage.\r\n\r\n![](Media/MainTemplates.png)\r\n\r\nA template is composed of Name, Custom Features, Include Filters, Exclude Filters and Export Base. Name has to be unique; in case of conflict, duplicate templates will be ignored. Custom Features are equivalent of the custom features in Features tab of export dialog, while Filters are equivalent of Include/Exclude Filters in Resources tab. Images for reference:\r\n\r\n![](Media/ExportFilters.png) ![](Media/ExportFeatures.png)\r\n\r\nExport Base is a base directory used when exporting with this template. It's later prefixed with the actual file (see [Export Project From Template](#export-project-from-template)).\r\n\r\nTemplates can inherit other templates. Inheriting template will include features and filters of the parent and auto-update them, so you can e.g. make one configuration with your include filters and make all other templates use it. You no longer have to worry about updating all export presets when you add a new file extension. Export Base is not inherited.\r\n\r\n### Tasks\r\n\r\nThe third tab in the main screen. It's a readonly list of all available tasks, with their descriptions and argument list. You can use it as a quick reference of what does what. More detailed information is available in this README at [List of Available Tasks](#list-of-available-tasks).\r\n\r\n![](Media/MainTasks.png)\r\n\r\nThe tasks are listed from the Tasks directory of Project Builder. Each task is a separate scene with the root inheriting a special Task class, which extends Control. All configuration controls are part of the task scene.\r\n\r\n#### Custom Tasks\r\n\r\nYou can create your own tasks by adding new scenes to the Tasks folder. The easiest way to create a Task is by opening the Project Builder source project and creating a new scene using Task class for root. There is a script template that makes it easier.\r\n\r\nWhen implementing methods, refer to the documentation of Task class and the existing tasks implementation (the default tasks have their code in built-in scripts). If you are using a stand-alone version of the Project Builder, you'll have to copy your new task to the Tasks folder of your installation. In the future there might be support for project local tasks, but currently this feature is not a priority.\r\n\r\n### Config\r\n\r\nThe last tab in the main screen. Here you can configure Project Builder. It includes both global config that applies to all your projects and a local config, which is per-project. The local config is stored in the builds config file mentioned before, while global config is stored in Project Builder's user data folder.\r\n\r\n![](Media/MainConfig.png)\r\n\r\nBoth configurations are also organized into foldable tabs of related settings.\r\n\r\n#### Global Configuration\r\n\r\n- **Godot**\r\n    - **Godot Path:** Path to the Godot executable. It will be used for exporting projects.\r\n\r\n- **Steam**\r\n    - **Steam CMD Path:** Path to `steamcmd.exe` that comes with Steam SDK. It's needed if you want to publish builds to Steam.\r\n    - **Username:** Login used for authentication when uploading games. Note that Project Builder does not support console input, so you can't use account that uses Steam mobile app for authentication (it requires inputting a code with each login). E-mail 2FA only requires single authentication, which you can do externally.\r\n    - **Password:** Password for the above account. Project Builder will automatically hide password when executing a command, but while the password is not stored in the project folder, it can be found in the global configuration file of Project Builder.\r\n\r\n- **GOG**\r\n    - **Pipeline Builder Path:** Path to Pipeline Builder executable used to publish builds to GOG.\r\n    - **Username:** Used for authentication when uploading games, just like in Steam.\r\n    - **Password:** Password for the above account.\r\n\r\n- **Epic**\r\n    - **Build Patch Tool Path:** Path to Build Patch Tool executable used to publish builds to Epic Games.\r\n    - **Organization ID**, **Client ID:** The organization and client ID provided for your developer account.\r\n    - **Client Secret:** The client secret provided for your developer account. This is a sensitive data, like the Steam and GOG passwords.\r\n    - **Client Secret Env Variable:** Name of the environment variable that contains your Client Secret. This is an alternate authentication method supported by Build Patch Tool and it will be used instead of Client Secret if provided.\r\n\r\n- **Itch**\r\n    - **Butler Path:** Path to itch.io's Butler executable used to publish builds to itch.io.\r\n    - **Username:** Login used for authentication when uploading games. Note that unlike the other upload tools, you have to launch butler and login to cache your credentials (this is not handled by Project Builder). It has to be done once.\r\n\r\n#### Local Configuration\r\n\r\n- **Godot**\r\n    - **Project Builder Configuration Path:** Path to Project Builder configuration file (by default `project_builds_config.txt`). You need to press Apply button to actually change this setting. Doing so will also automatically move the file. Unlike other local settings, this one is stored in `project.godot`, under hidden `_project_builder_config_path` setting. Note that in Local Config the path is not editable and you can change only the directory. Manually changing the path is not advised.\r\n    - **Godot Exec For This Project:** Executable used for exporting the current project. If you leave this empty, the default one will be used. This option is useful when you have projects using different Godot versions.\r\n\r\n- **Epic**\r\n    - **Product ID**, **Artifact ID:** Product and artifact IDs provided for your game.\r\n    - **Cloud Directory:** Directory required by Build Patch Tool for its operation. It's effectively a cache directory, so you can add it to your VCS ignore list.\r\n\r\n- **Itch**\r\n    - **Game Name:** Name of your game. This has to match the itch.io page (e.g. if your game is at `username.itch.io/my_game`, Game Name should be `my_game`).\r\n    - **Default Channel:** App channel to which the game will be uploaded if not specified by the task.\r\n    - **Version File:** File to use for `userversion-file` argument. If not provided, the argument will not be passed. If \"Use Project Version\" is enabled in upload task, the file is ignored.\r\n\r\n#### Project Scan\r\n\r\nAt the bottom of the Config tab is a Run Project Scan button. It launches a scan of all files in your project. This is required by some tasks and will run automatically when the project is opened for the first time. If a task is relying on scan results (as noted [in its description](#list-of-available-tasks)), you may want to run the scan if the data is outdated. It has to be done manually, for performance reasons (crawling every file in the project can be expensive).\r\n\r\n#### Required Paths\r\n\r\nThis applies to tasks and configuration. You will sometimes notice that a file/directory field is marked yellow or red. This denotes required fields. If a field is yellow, it means that the target file/directory must exist at the time the task is executed, so at most it has to be created as a result of preceding tasks. If a field is red it means that the task will fail at the prevalidation stage.\r\n\r\n![](Media/DirectoryWarning.png)![](Media/DirectoryError.png)\r\n\r\n### Project Builder Plugin\r\n\r\nProject Builder has an optional plugin that allows for better integration with your project. Using the plugin you can change the location of your project's build config file and run Project Builder directly for your project. At the bottom of the Config tab you can find the plugin status in your project:\r\n\r\n![](Media/PluginStatus.png)\r\n\r\nIt will tell you whether the plugin is installed or not, and if it needs update. If the plugin is not installed or outdated, you can click Install Project Builder Plugin to add the plugin to your project (you'll need to manually enable it in Project Settings).\r\n\r\nThe addon also adds a Project Builder submenu to Project -> Tools menu. From there you can run Project Builder or directly execute any of the registered routines. These options are also added to Command Palette under Project Builder category.\r\n\r\n### Running From Command Line\r\n\r\nWhile Project Builder has a visual execution mode, you can use it as a tool for configuring automated tasks and run them from command line. Project Builder supports a number of command line arguments. You need to pass them after `--` in your command. These arguments are:\r\n- `--projects-file-path`: Overrides the path of project list file.\r\n- `--open-project <project>`: Starts Project Builder and opens the provided `project`.\r\n- `--execute-routine <routine>`: On launch, executes the routine `routine`.\r\n- `--exit`: If executing routine, automatically exits after finishing or error.\r\n\r\nExample line for running builder for project at `C:/My Project` in headless mode, executing routine `Export Game` and exiting:\r\n```\r\ngodot --path \"C:/Project Builder\" -- --open-project \"C:/My Project\" --execute_routine \"Export Game\" --exit\r\n```\r\nThe `--path` part can be omitted when running from Project Builder installation directory.\r\n\r\n\r\n## List of Available Tasks\r\n\r\nThis sections lists all default tasks shipped with Project Builder.\r\n\r\n### Clear Directory Files\r\n\r\n![](Media/TaskClearDirectoryFiles.png)\r\n\r\nRemoves all files in a directory. The files are not deleted permanently, instead they are moved to a dedicated `Trash` directory in Project Builder's user data. If you used this task accidentally or want to recover files, you can find them in that directory. Note that Trash holds only one copy of the file, so if you \"delete\" it again, it will be overwritten.\r\n\r\nThis task is useful for preparing export directory, to make sure that it doesn't have lefotver files.\r\n\r\n**Options**\r\n- **Target Directory:** The directory in your project that's going to be cleaned.\r\n- **Include Files:** Filters applied to each processed file to determine whether it should be included. Use it when you want to delete only some files of the directory. If empty, files will be included by default.\r\n- **Exclude Files:** Filters applied to each processed file to determine whether it should be excluded. Use it when your directory has files that shouldn't be deleted. If empty, no file will be excluded by default.\r\n\r\n### Copy Files(s)\r\n\r\n![](Media/TaskCopyFiles.png)\r\n\r\nCopies a file or directory from source path to destination. It can also recursively copy whole directory. If files already exist at the target location, they will be overwritten. If the target directory does not exist, it will be created.\r\n\r\nUseful for copying additional files not included with export.\r\n\r\n**Options**\r\n- **Source Path:** The source path to copy. If a file is selected, only this file will be copied. If a directory is selected, all files will be copied to the target location.\r\n- **Target Path:** The destination where the files will be copied. If source is a file and target is a directory, the file will be copied to the directory and keep its name. If target is a file, the file will be copied and renamed to the new name. If source is a directory, all files will be copied to the target directory.\r\n- **Recursive:** When copying directory, this option enables copying sub-directories.\r\n\r\n### Custom Task\r\n\r\n![](Media/TaskCustomTask.png)\r\n\r\nAn arbitrary task for operations not covered by other tasks, to use when it's not worth it to make a new task type. The task allows to specify a raw command and argument list that will be invoked during execution. It's very flexible, but requires manually providing all necessary data.\r\n\r\nNote that working directory is undefined, so you should use absolute paths both for command and arguments. You can use `$project` placeholder, which will be automatically substituted with path to the current project, e.g. `$project/.godot`.\r\n\r\n**Options**\r\n- **Command:** Command that will be executed. It will be invoked like from a terminal. You can use `$godot` or `$local_godot` special names to use current Godot executable or project-configured Godot executable respectively.\r\n- **Arguments:** List of arguments provided for the command. They are automatically wrapped in quotes when necessary.\r\n\r\n### Export Project\r\n\r\n![](Media/TaskExportProject.png)\r\n\r\nExports the project using the given preset (it will list the presets defined in `export_presets.cfg`). Also allows to provide custom export path. If the target directory does not exist, it will be automatically created. This task uses the Godot executable specified in [config](#config). If you are exporting a Godot 3 project, make sure to provide a Godot 3 executable.\r\n\r\n**Options**\r\n- **Preset:** Export preset used to export the project.\r\n- **Custom Path:** If provided, the preset's path will be overriden. If empty, the default path will be used.\r\n- **Debug:** Determines whether debug or release build is exported.\r\n\r\n### Export Project From Template\r\n\r\n![](Media/TaskExportProjectFromTemplate.png)\r\n\r\nExports the project using the given base preset and [preset template](#preset-templates). It works by temporarily adding a new preset to `export_presets.cfg` file. The new preset will be a copy of the base preset with some properties modified based on the template.\r\n\r\n**Options**\r\n- **Base Preset:** Base export preset from the project's preset list.\r\n- **Preset Template:** The template from the list of templates defined in Project Builder.\r\n- **Path Suffix:** Partial path that will be joined with Export Base property of the [template](#preset-templates). E.g. you can set the base path to `Export/Steam` and then you can specify suffix based on platform, like `Windows/MyGame.exe` (suffix has to include the file name). If Export Base was not defined, the default path from export preset will be used.\r\n- **Debug:** Determines whether debug or release build is exported.\r\n\r\n### Pack ZIP\r\n\r\n![](Media/TaskPackZIP.png)\r\n\r\nPacks the given directory (including subfolders) to a ZIP archive. You can specify include and exclude filters for files, which work using globbing. You can define both include and exclude filters and they will be both evaluated for each file.\r\n\r\nUseful when you want to pack an exported project to share it. The ZIP is created using Godot's ZIPPacker class.\r\n\r\n**Options**\r\n- **Source Directory:** The directory which is going to be packed. The ZIP will have the same structure.\r\n- **Target File Path:** The path to the resulting ZIP file.\r\n- **Include Files:** Filters applied to each processed file to determine whether it should be included. Use it when you want to pack only some files of the directory. If empty, files will be included by default.\r\n- **Exclude Files:** Filters applied to each processed file to determine whether it should be excluded. Use it when your directory has files that shouldn't be packed. If empty, no file will be excluded by default.\r\n\r\n### Sub-Routine\r\n\r\n![](Media/TaskSubRoutine.png)\r\n\r\nIncludes another routine as a task. The other routine's tasks will be seamlessly added to the execution list. This is useful when you e.g. have export and upload in separate routines, but want one that does everything.\r\n\r\n**Options**\r\n- **Routine:** The routine that will be processed by this task, from the list of routines defined in your project.\r\n\r\n### Upload Epic\r\n\r\n![](Media/TaskUploadEpic.png)\r\n\r\nUploads files to Epic Games using Build Patch Tool. Uploading to Epic requires executing a lengthy command, so this task is especially helpful in getting it right. Before using it you need to fill Epic sections in your global and local configs.\r\n\r\nThis task uses the project's version, which is read from `application/config/version` Project Setting of your project. Make sure it's unique for each upload. You can use [my auto versioning addon](https://github.com/KoBeWi/Godot-Auto-Export-Version) to handle that easily ;) Note that Epic imposes some limits on the version string (related to available characters and length). Refer to the [Build Patch Tool manual](https://dev.epicgames.com/docs/epic-games-store/publishing-tools/uploading-binaries/bpt-instructions-150#how-to-upload-a-binary) for details, but unless you use crazy version strings, you won't run into a problem.\r\n\r\n**Options**\r\n- **Build Root:** The root directory of the files you want to upload.\r\n- **Executable Name:** The executable file used for launching the application (relative to the root directory).\r\n- **Version Prefix:** String prefixed to your project's version. Epic requires each upload to have unique version, so you can define a per-platform prefix for each task. It will be appended to your project version with a dash, e.g. if your project has version `1.0` and prefix is `win`, the uploaded version will be `1.0-win`.\r\n\r\n### Upload GOG\r\n\r\n![](Media/TaskUploadGOG.png)\r\n\r\nUploads files to GOG using Pipeline Builder. This task has no configuration in itself, you only pick the GOG's JSON configuration file that will be used. Make sure to fill the GOG global configuration before using this task. Like Upload Epic, this task uses project's version.\r\n\r\n**Options**\r\n- **JSON File:** The JSON file used for upload. The list will show only file names, their full path is in tooltip. The list of JSONs will automatically include all GOG's JSON files in your project [when the project is scanned](#project-scan).\r\n- **Branch:** Optional branch where the files will be uploaded.\r\n- **Branch Password:** Password for the branch, if it's protected.\r\n\r\n### Upload Itch\r\n\r\n![](Media/TaskUploadItch.png)\r\n\r\nUploads files to itch.io using butler. Before using it, fill the global and local configuration for Itch and make sure butler has cached credentials.\r\n\r\n**Options**\r\n- **Source Folder:** The folder containing your exported project files.\r\n- **Channel:** Channel where the files will be uploaded. The name is semi-important, refer to [butler's manual](https://itch.io/docs/butler/pushing.html).\r\n\r\n### Upload Steam\r\n\r\n![](Media/TaskUploadSteam.png)\r\n\r\nUploads files to Steam using Steam CMD. Just like GOG, the whole setup is inside configuration file (but here it's VDF).\r\n\r\n**Options**\r\n- **VDF File:** The VDF file used for upload. Same as JSON in Upload GOG task.\r\n\r\n## Closing Words\r\n\r\nProject Builder evolved from build tools I created for Lumencraft. I wrote a small build system in Python, but eventually it got so many options that running from command line got annoying. From this experience I came up with a system where you can construct your various tasks from predefined building blocks, which are way easier to use.\r\n\r\nIf you have ideas for more useful tasks, feel free to suggest them in the Issues page.\r\n\r\n___\r\nYou can find all my addons on my [profile page](https://github.com/KoBeWi).\r\n\r\n<a href='https://ko-fi.com/W7W7AD4W4' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>\r\n"
  },
  {
    "path": "Scenes/Execution.gd",
    "content": "extends Control\n\nsignal finished\n\n@onready var task_limbo: Node2D = %TaskLimbo\n@onready var commands_container: VBoxContainer = %CommandsContainer\n@onready var error_container: Control = %Errors\n\nvar routine: Dictionary\n\nvar current_task_index: int\nvar current_task: Task\nvar task_in_progress: bool\nvar task_error: String\n\nvar on_fail: int\nvar sensitive_strings: PackedStringArray\nvar log_file: FileAccess\nvar fail_count: int\n\nvar separator_prefab: PackedScene\n\nfunc _ready() -> void:\n\tseparator_prefab = Prefab.create(%Separator)\n\t\n\tvar errors: PackedStringArray\n\t\n\troutine = Data.current_routine\n\ton_fail = routine[\"on_fail\"]\n\t\n\tfor task in routine[\"tasks\"]:\n\t\tvar task_instance := Task.create_instance(task[\"scene\"])\n\t\ttask_limbo.add_child(task_instance)\n\t\t\n\t\ttask_instance._initialize()\n\t\ttask_instance.load_data(task[\"data\"])\n\t\t\n\t\tif not task_instance._prevalidate():\n\t\t\terrors.append(\"%s: %s\" % [task_instance._get_execute_string(), task_instance.error_message])\n\t\n\tvar wait := 2\n\tif not errors.is_empty():\n\t\t%ErrorsParent.show()\n\t\t%Delay.hide()\n\t\t\n\t\tfor error in errors:\n\t\t\tvar label := Label.new()\n\t\t\tlabel.text = error\n\t\t\tlabel.modulate = Color.RED\n\t\t\terror_container.add_child(label)\n\t\t\n\t\tfinish()\n\t\treturn\n\telse:\n\t\twait = Data.global_config[\"execution_delay\"]\n\t\t%Delay.text %= wait\n\t\n\tif wait > 0:\n\t\tawait get_tree().create_timer(wait).timeout\n\t%Delay.hide()\n\t\n\tDirAccess.make_dir_recursive_absolute(\"user://BuildLogs\")\n\tvar logs: Array[String]\n\tlogs.assign(DirAccess.get_files_at(\"user://BuildLogs\"))\n\tif logs.size() >= 10:\n\t\tlogs.sort_custom(func(log1: String, log2: String):\n\t\t\treturn FileAccess.get_modified_time(\"user://BuildLogs\".path_join(log1)))\n\t\t\n\t\tfor logg in logs.slice(9):\n\t\t\tDirAccess.remove_absolute(\"user://BuildLogs\".path_join(logg))\n\t\n\tfor setting in Data.sensitive_settings:\n\t\tvar string: String = Data.global_config.get(setting, \"\")\n\t\tif string.is_empty():\n\t\t\tstring = Data.local_config.get(setting, \"\")\n\t\t\n\t\tif not string.is_empty():\n\t\t\tsensitive_strings.append(string)\n\t\n\tvar filename := \"user://\" + (\"BuildLogs/Log-%s.log\" % Time.get_datetime_string_from_system()).replace(\":\", \"-\")\n\tlog_file = FileAccess.open(filename, FileAccess.WRITE)\n\tnext_command()\n\nfunc next_command():\n\tif current_task_index == task_limbo.get_child_count():\n\t\tfinish()\n\t\treturn\n\tlog_file.store_line(\"\")\n\t\n\tcurrent_task = task_limbo.get_child(current_task_index)\n\tvar command := preload(\"res://Nodes/Command.tscn\").instantiate()\n\tcommand.log_file = log_file\n\t\n\tif current_task._validate():\n\t\ttask_in_progress = true\n\t\tcurrent_task._prepare()\n\telse:\n\t\tcommand.error = current_task.error_message\n\t\n\tcommand.task_text = current_task._get_execute_string()\n\tcommand.command = current_task._get_command()\n\tcommand.arguments = current_task._get_arguments()\n\tif current_task.has_sensitive_data:\n\t\tcommand.sensitive_strings = sensitive_strings\n\t\n\tcommand.success.connect(task_finished.bind(true), CONNECT_ONE_SHOT | CONNECT_DEFERRED)\n\tcommand.fail.connect(task_finished.bind(false), CONNECT_ONE_SHOT | CONNECT_DEFERRED)\n\tcommands_container.add_child(command)\n\t\n\tcurrent_task_index += 1\n\nfunc task_finished(success: bool):\n\tcurrent_task._cleanup()\n\ttask_in_progress = false\n\t\n\tvar command := commands_container.get_child(-1)\n\tvar intime: int = command.timer\n\tlog_file.store_line(\"\\n> Finished with code %d, time: %02d:%02d:%02d.\" % [command.finish_code, intime / 3600, intime / 60 % 60, intime % 60])\n\tlog_file.flush()\n\t\n\tcommands_container.add_child(separator_prefab.instantiate())\n\t\n\tif not success:\n\t\tfail_count += 1\n\t\tif on_fail == 0:\n\t\t\tfinish()\n\t\t\treturn\n\t\n\tnext_command()\n\nfunc finish():\n\tfinished.emit()\n\t\n\tif Data.auto_exit:\n\t\tget_tree().quit(fail_count)\n\t\treturn\n\t\n\t%Button.show()\n\t\n\tvar total_time: float\n\tfor command in commands_container.get_children():\n\t\tif &\"timer\" in command:\n\t\t\ttotal_time += command.timer\n\t\n\tvar intime := int(total_time)\n\t%Time.text %= [intime / 3600, intime / 60 % 60, intime % 60]\n\t%Time.show()\n\nfunc go_back() -> void:\n\tget_tree().change_scene_to_packed(Data.main)\n\nfunc _exit_tree() -> void:\n\tData.reset_current_routine()\n\t\n\tif task_in_progress:\n\t\tcurrent_task._cleanup()\n"
  },
  {
    "path": "Scenes/Execution.gd.uid",
    "content": "uid://gj2kvs6p5asi\n"
  },
  {
    "path": "Scenes/Execution.tscn",
    "content": "[gd_scene load_steps=6 format=3 uid=\"uid://dqm1wdopgkdkp\"]\n\n[ext_resource type=\"Script\" uid=\"uid://gj2kvs6p5asi\" path=\"res://Scenes/Execution.gd\" id=\"1_rwjih\"]\n[ext_resource type=\"Shortcut\" uid=\"uid://d0olqx0bjlhp1\" path=\"res://Nodes/GUI/ExitShortcut.tres\" id=\"2_qbdqm\"]\n\n[sub_resource type=\"StyleBoxEmpty\" id=\"StyleBoxEmpty_6oq72\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_tsvn6\"]\ncontent_margin_left = 8.0\ncontent_margin_top = 8.0\ncontent_margin_right = 8.0\ncontent_margin_bottom = 8.0\nbg_color = Color(0, 0, 0, 0.501961)\n\n[sub_resource type=\"StyleBoxLine\" id=\"StyleBoxLine_ykpnb\"]\ncolor = Color(1, 1, 1, 1)\ngrow_begin = -50.0\ngrow_end = -50.0\nthickness = 2\n\n[node name=\"Execution\" type=\"ScrollContainer\"]\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nsize_flags_vertical = 3\nscript = ExtResource(\"1_rwjih\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Delay\" type=\"Label\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"Starting in %d seconds.\nPress Escape to cancel.\"\nhorizontal_alignment = 1\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer/Delay\"]\nlayout_mode = 0\ntheme_override_styles/normal = SubResource(\"StyleBoxEmpty_6oq72\")\nshortcut = ExtResource(\"2_qbdqm\")\n\n[node name=\"ErrorsParent\" type=\"PanelContainer\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nvisible = false\nlayout_mode = 2\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_tsvn6\")\n\n[node name=\"Errors\" type=\"VBoxContainer\" parent=\"VBoxContainer/ErrorsParent\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"VBoxContainer/ErrorsParent/Errors\"]\nmodulate = Color(1, 0, 0, 1)\nlayout_mode = 2\ntext = \"Some tasks are invalid!\"\nhorizontal_alignment = 1\n\n[node name=\"CommandsContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Separator\" type=\"HSeparator\" parent=\"VBoxContainer/CommandsContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntheme_override_constants/separation = 30\ntheme_override_styles/separator = SubResource(\"StyleBoxLine_ykpnb\")\n\n[node name=\"Time\" type=\"Label\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nvisible = false\nlayout_mode = 2\ntheme_override_font_sizes/font_size = 30\ntext = \"Finished. Total time: %02d:%02d:%02d.\"\nhorizontal_alignment = 1\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nvisible = false\nlayout_mode = 2\nsize_flags_horizontal = 4\nsize_flags_vertical = 8\nshortcut = ExtResource(\"2_qbdqm\")\nshortcut_in_tooltip = false\ntext = \"< Back\"\n\n[node name=\"TaskLimbo\" type=\"Node2D\" parent=\".\"]\nunique_name_in_owner = true\nvisible = false\n\n[connection signal=\"pressed\" from=\"VBoxContainer/Delay/Button\" to=\".\" method=\"go_back\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/Button\" to=\".\" method=\"go_back\"]\n"
  },
  {
    "path": "Scenes/Main.gd",
    "content": "extends Control\n\n@onready var template_container: Control = %TemplateContainer\n@onready var routine_container: Control = %RoutineContainer\n@onready var task_container: Control = %TaskContainer\n\nvar task_queue: Array[Dictionary]\nvar task_queue_index: int\n\nfunc _ready() -> void:\n\tvar config := ConfigFile.new()\n\tconfig.load(Data.project_path.path_join(\"project.godot\"))\n\t\n\t%Title.text %= config.get_value(\"application\", \"config/name\", \"[unnamed]\")\n\tif Data.from_plugin:\n\t\t%Back.hide()\n\t\n\tfor routine in Data.routines:\n\t\tadd_routine(routine)\n\t\n\tfor template in Data.templates:\n\t\tvar temp := _add_template_pressed()\n\t\ttemp.set_data(template)\n\t\n\ttask_queue.assign(Data.tasks.values())\n\tset_physics_process(false)\n\t\n\tif Data.first_load:\n\t\tfor task in Data.static_initialize_tasks:\n\t\t\ttask._initialize_project()\n\t\tData.first_load = false\n\t\t\n\t\tif Data.initial_load:\n\t\t\trun_project_scan()\n\t\t\tData.initial_load = false\n\nfunc process_files(directory: String):\n\tfor file in DirAccess.get_files_at(directory):\n\t\tfor task in Data.static_initialize_tasks:\n\t\t\ttask._process_file(directory.path_join(file))\n\t\n\tfor dir in DirAccess.get_directories_at(directory):\n\t\tif not dir.begins_with(\".\"):\n\t\t\tprocess_files(directory.path_join(dir))\n\nfunc _physics_process(delta: float) -> void:\n\tvar task := task_queue[task_queue_index]\n\t\n\tvar preview := preload(\"res://Nodes/TaskPreview.tscn\").instantiate()\n\tpreview.task = task\n\ttask_container.add_child(preview)\n\t\n\ttask_queue_index += 1\n\tif task_queue_index == task_queue.size():\n\t\tset_physics_process(false)\n\t\ttask_queue.clear()\n\nfunc _exit_tree() -> void:\n\tif Data.project_path.is_empty():\n\t\treturn\n\tsync_templates()\n\nfunc _add_template_pressed() -> Control:\n\tvar template := preload(\"res://Nodes/PresetTemplate.tscn\").instantiate()\n\ttemplate_container.add_child(template)\n\t\n\ttemplate.connect_duplicate(duplicate_template.bind(template))\n\ttemplate.connect_inherit(inherit_template.bind(template))\n\ttemplate.connect_delete(delete_template.bind(template))\n\treturn template\n\nfunc _add_routine_pressed() -> void:\n\tadd_routine(Data.create_routine())\n\nfunc add_routine(data: Dictionary):\n\tvar routine := preload(\"res://Nodes/RoutinePreview.tscn\").instantiate()\n\troutine_container.add_child(routine)\n\troutine.owner = self\n\troutine.set_routine_data(data)\n\troutine.connect_execute(exec_routine.bind(data))\n\troutine.connect_edit(edit_routine.bind(data))\n\troutine.connect_duplicate(duplicate_routine.bind(data))\n\nfunc exec_routine(data: Dictionary):\n\tData.current_routine = data\n\tget_tree().change_scene_to_file(\"res://Scenes/Execution.tscn\")\n\nfunc edit_routine(data: Dictionary):\n\tData.current_routine = data\n\tget_tree().change_scene_to_file(\"res://Scenes/RoutineBuilder.tscn\")\n\nfunc duplicate_routine(data: Dictionary):\n\tdata = data.duplicate(true)\n\t\n\tvar new_name: String = data[\"name\"]\n\tvar suffix := \" (Copy)\"\n\t\n\tvar unique: bool\n\twhile not unique:\n\t\tunique = true\n\t\tfor routine in Data.routines:\n\t\t\tif routine[\"name\"] == new_name + suffix:\n\t\t\t\tsuffix = \" (Copy %d)\" % (suffix.to_int() + 1)\n\t\t\t\tunique = false\n\t\n\tdata[\"name\"] = new_name + suffix\n\tadd_routine(data)\n\t\n\tsync_routines()\n\tData.queue_save_local_config()\n\nfunc duplicate_template(template: Control):\n\tvar dup := _add_template_pressed()\n\tvar data: Dictionary = template.get_data().duplicate()\n\tdata[\"name\"] = Data.get_unique_name(Data.templates, data[\"name\"], \"(Copy %d)\", 0)\n\t\n\tdup.set_data(data)\n\tsync_templates()\n\tData.save_local_config()\n\t\n\tfor other in template_container.get_children():\n\t\tif other.get_index() > template.get_index() and other.inherit != template.get_template_name():\n\t\t\ttemplate_container.move_child(dup, other.get_index())\n\t\t\tbreak\n\t\n\tawait get_tree().process_frame\n\t%TemplateScroll.ensure_control_visible(dup)\n\nfunc inherit_template(template: Control):\n\tvar dup := _add_template_pressed()\n\tvar data: Dictionary = template.get_data().duplicate()\n\tdata[\"inherit\"] = data[\"name\"]\n\tdata[\"name\"] = Data.get_unique_name(Data.templates, data[\"name\"], \"(Inherited %d)\", 0)\n\t\n\tdup.set_data(data)\n\tsync_templates()\n\tData.save_local_config()\n\t\n\tfor other in template_container.get_children():\n\t\tif other.get_index() > template.get_index() and other.inherit != template.get_template_name():\n\t\t\ttemplate_container.move_child(dup, other.get_index())\n\t\t\tbreak\n\t\n\tawait get_tree().process_frame\n\t%TemplateScroll.ensure_control_visible(dup)\n\nfunc delete_template(template: Control):\n\ttemplate.queue_free()\n\tsync_templates()\n\tData.queue_save_local_config()\n\nfunc sync_routines():\n\tData.routines.assign(routine_container.get_children().map(func(routine: Control) -> Dictionary:\n\t\treturn routine.data))\n\nfunc sync_templates():\n\tData.templates.assign(template_container.get_children().map(func(template: Control) -> Dictionary:\n\t\treturn template.get_data()))\n\nfunc go_back() -> void:\n\tsync_templates()\n\tData.save_local_config()\n\tData.project_path = \"\"\n\tget_tree().change_scene_to_file(\"res://Scenes/ProjectManager.tscn\")\n\nfunc run_project_scan() -> void:\n\tfor task in Data.static_initialize_tasks:\n\t\ttask._begin_project_scan()\n\t\n\tprocess_files(Data.project_path)\n\t\n\tfor task in Data.static_initialize_tasks:\n\t\ttask._end_project_scan()\n\t\n\t%ScanFinished.show()\n\t%ScanFinished.modulate.a = 1.0\n\t\n\tvar tween := create_tween()\n\ttween.tween_property(%ScanFinished, ^\"modulate:a\", 0.0, 0.5).set_delay(0.5)\n\ttween.tween_callback(%ScanFinished.hide)\n\nfunc tab_changed(tab: int) -> void:\n\tif tab == 2 and not task_queue.is_empty():\n\t\tset_physics_process(true)\n\nfunc open_logs() -> void:\n\tOS.shell_open(ProjectSettings.globalize_path(\"user://BuildLogs\"))\n"
  },
  {
    "path": "Scenes/Main.gd.uid",
    "content": "uid://bjg08mlxtbkx5\n"
  },
  {
    "path": "Scenes/Main.tscn",
    "content": "[gd_scene load_steps=19 format=3 uid=\"uid://ba3dlg1fj2hxv\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bjg08mlxtbkx5\" path=\"res://Scenes/Main.gd\" id=\"1_r1xkq\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dt6drd8cw1dhl\" path=\"res://Icons/Back.svg\" id=\"2_fue2w\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"2_ood4h\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://bidwkeg0fiqcn\" path=\"res://Icons/ArrowDown.svg\" id=\"3_50glp\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://6orgtpcieuls\" path=\"res://Icons/ArrowUp.svg\" id=\"3_jw32o\"]\n[ext_resource type=\"Shortcut\" uid=\"uid://d0olqx0bjlhp1\" path=\"res://Nodes/GUI/ExitShortcut.tres\" id=\"4_bgxb8\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://ckqqfrho1pqcd\" path=\"res://Icons/Script.svg\" id=\"6_kgk8y\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_x81aq\"]\nbg_color = Color(0, 0, 0, 0.12549)\nborder_width_top = 4\nborder_color = Color(0.16, 0.16, 0.16, 0.752941)\n\n[sub_resource type=\"GDScript\" id=\"GDScript_yepl2\"]\nresource_name = \"DebugDiscard\"\nscript/source = \"@tool\nextends Control\n\nfunc _validate_property(property: Dictionary) -> void:\n\tif property.name == \\\"current_tab\\\" or property.name == \\\"scroll_vertical\\\":\n\t\tproperty.usage = PROPERTY_USAGE_EDITOR\n\"\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_nuonq\"]\ncontent_margin_right = 4.0\nbg_color = Color(0, 0, 0, 0.12549)\n\n[sub_resource type=\"StyleBoxEmpty\" id=\"StyleBoxEmpty_cy4oc\"]\ncontent_margin_right = 4.0\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_jw32o\"]\ncontent_margin_left = 4.0\ncontent_margin_top = 4.0\ncontent_margin_right = 4.0\ncontent_margin_bottom = 4.0\nbg_color = Color(0.136, 0.61186665, 0.8, 0.6)\ncorner_radius_top_left = 3\ncorner_radius_top_right = 3\ncorner_radius_bottom_right = 3\ncorner_radius_bottom_left = 3\ncorner_detail = 5\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_v1gob\"]\ncontent_margin_left = 4.0\ncontent_margin_top = 4.0\ncontent_margin_right = 4.0\ncontent_margin_bottom = 4.0\nbg_color = Color(0.0969, 0.435955, 0.57, 0.6)\ncorner_radius_top_left = 3\ncorner_radius_top_right = 3\ncorner_radius_bottom_right = 3\ncorner_radius_bottom_left = 3\ncorner_detail = 5\n\n[sub_resource type=\"Theme\" id=\"Theme_v1gob\"]\nFoldableContainer/icons/expanded_arrow = ExtResource(\"3_jw32o\")\nFoldableContainer/icons/folded_arrow = ExtResource(\"3_50glp\")\nFoldableContainer/styles/title_collapsed_hover_panel = SubResource(\"StyleBoxFlat_jw32o\")\nFoldableContainer/styles/title_collapsed_panel = SubResource(\"StyleBoxFlat_v1gob\")\nFoldableContainer/styles/title_hover_panel = SubResource(\"StyleBoxFlat_jw32o\")\nFoldableContainer/styles/title_panel = SubResource(\"StyleBoxFlat_v1gob\")\n\n[sub_resource type=\"GDScript\" id=\"GDScript_rk2qg\"]\nresource_name = \"Config\"\nscript/source = \"extends HBoxContainer\n\nfunc _ready() -> void:\n\tregister_global_setting(%GlobalGodot, \\\"godot_path\\\", OS.get_executable_path())\n\tregister_global_setting(%ExecutionDelay, \\\"execution_delay\\\", 2)\n\t\n\tregister_global_setting(%SteamCMD, \\\"steam_cmd_path\\\", \\\"\\\")\n\tregister_global_setting(%SteamUsername, \\\"steam_username\\\", \\\"\\\")\n\tregister_global_setting(%SteamPassword, \\\"steam_password\\\", \\\"\\\", true)\n\t\n\tregister_global_setting(%PipelineBuilderPath, \\\"pipeline_builder_path\\\", \\\"\\\")\n\tregister_global_setting(%GOGUsername, \\\"gog_username\\\", \\\"\\\")\n\tregister_global_setting(%GOGPassword, \\\"gog_password\\\", \\\"\\\", true)\n\t\n\tregister_global_setting(%BuildPatchToolPath, \\\"build_patch_tool_path\\\", \\\"\\\")\n\tregister_global_setting(%EpicOrganizationID, \\\"epic_organization_id\\\", \\\"\\\")\n\tregister_global_setting(%EpicClientID, \\\"epic_client_id\\\", \\\"\\\")\n\tregister_global_setting(%EpicClientSecret, \\\"epic_client_secret\\\", \\\"\\\", true)\n\tregister_global_setting(%EpicClientSecretEnvVar, \\\"epic_client_secret_env_var\\\", \\\"\\\")\n\t\n\tregister_global_setting(%ItchButlerPath, \\\"itch_butler_path\\\", \\\"\\\")\n\tregister_global_setting(%ItchUsername, \\\"itch_username\\\", \\\"\\\")\n\t\n\tregister_local_setting(%LocalGodot, \\\"godot_path\\\", \\\"\\\")\n\t\n\tregister_local_setting(%EpicProductID, \\\"epic_product_id\\\", \\\"\\\")\n\tregister_local_setting(%EpicArtifactID, \\\"epic_artifact_id\\\", \\\"\\\")\n\tregister_local_setting(%EpicCloudDir, \\\"epic_cloud_dir\\\", \\\"\\\")\n\t\n\tregister_local_setting(%ItchGameName, \\\"itch_game_name\\\", \\\"\\\")\n\tregister_local_setting(%ItchDefaultChannel, \\\"itch_default_channel\\\", \\\"\\\")\n\tregister_local_setting(%ItchVersionFile, \\\"itch_version_file\\\", \\\"\\\")\n\t\n\t%ConfigPath.text = Data.local_config_file\n\t%ConfigPath.path_changed.connect(config_path_updated, CONNECT_DEFERRED)\n\t\n\tupdate_plugin_status()\n\nfunc register_global_setting(control: Control, setting: String, default: Variant, sensitive := false):\n\tregister_setting(control, setting, default, Data.global_config, on_global_setting_changed, sensitive)\n\nfunc register_local_setting(control: Control, setting: String, default: Variant, sensitive := false):\n\tregister_setting(control, setting, default, Data.local_config, on_local_setting_changed, sensitive)\n\nfunc register_setting(control: Control, setting: String, default: Variant, config: Dictionary, callback: Callable, sensitive: bool):\n\tif setting in config:\n\t\tcontrol.text = config[setting]\n\telse:\n\t\tconfig[setting] = default\n\t\n\tvar sygnał\n\tvar binds: Array\n\t\n\tsygnał = control.get(&\\\"path_changed\\\")\n\tif sygnał:\n\t\tbinds.append(\\\"\\\")\n\telse:\n\t\tsygnał = control.get(&\\\"text_changed\\\")\n\t\tif not sygnał:\n\t\t\tsygnał = control.get(&\\\"value_changed\\\")\n\t\n\tassert(not sygnał.is_null())\n\t\n\tbinds.append_array([control, setting])\n\tsygnał.connect(callback.bindv(binds))\n\t\n\tif sensitive and not setting in Data.sensitive_settings:\n\t\tData.sensitive_settings.append(setting)\n\nfunc on_global_setting_changed(dummy, control: Control, setting: String):\n\tData.global_config[setting] = control.text\n\tData.queue_save_global_config()\n\nfunc on_local_setting_changed(dummy, control: Control, setting: String):\n\tData.local_config[setting] = control.text\n\tData.queue_save_local_config()\n\nfunc update_plugin_status():\n\tvar plugin_file := ConfigFile.new()\n\tplugin_file.load(Data.get_res_path().path_join(\\\"addons/ProjectBuilder/plugin.cfg\\\"))\n\tvar current_version: String = plugin_file.get_value(\\\"plugin\\\", \\\"version\\\", \\\"0.0\\\")\n\t\n\tif plugin_file.load(Data.project_path.path_join(\\\"addons/ProjectBuilder/plugin.cfg\\\")) == OK:\n\t\tvar project_version: String = plugin_file.get_value(\\\"plugin\\\", \\\"version\\\", \\\"0.0\\\")\n\t\tvar old: bool\n\t\t\n\t\tfor i in current_version.get_slice_count(\\\".\\\"):\n\t\t\tif current_version.get_slice(\\\".\\\", i).to_int() > project_version.get_slice(\\\".\\\", i).to_int():\n\t\t\t\told = true\n\t\t\t\tbreak\n\t\t\n\t\tif old:\n\t\t\t%PluginStatus.text = \\\"Plugin outdated.\\\"\n\t\t\t%PluginStatus.modulate = Color.YELLOW\n\t\t\t%InstallPlugin.disabled = false\n\t\telse:\n\t\t\t%PluginStatus.text = \\\"Plugin installed and up-to-date.\\\"\n\t\t\t%PluginStatus.modulate = Color.GREEN\n\t\t\t%InstallPlugin.disabled = true\n\telse:\n\t\t%PluginStatus.text = \\\"Plugin not installed.\\\"\n\t\t%PluginStatus.modulate = Color.RED\n\t\t%InstallPlugin.disabled = false\n\nfunc _on_install_plugin_pressed() -> void:\n\tvar source_path := Data.get_res_path().path_join(\\\"addons/ProjectBuilder\\\")\n\tvar target_path := Data.project_path.path_join(\\\"addons/ProjectBuilder\\\")\n\tDirAccess.make_dir_recursive_absolute(target_path)\n\t\n\tfor file in DirAccess.get_files_at(source_path):\n\t\tDirAccess.copy_absolute(source_path.path_join(file), target_path.path_join(file))\n\t\n\tupdate_plugin_status()\n\nfunc config_path_updated():\n\tvar old_path := Data.local_config_file\n\tvar show_error = func(text: String):\n\t\t%ConfigPath.text = old_path\n\t\t%PathError.dialog_text = text\n\t\t%PathError.popup_centered()\n\t\n\tvar new_path: String = %ConfigPath.text.path_join(\\\"project_builds_config.txt\\\")\n\tvar new_path_abs := Data.project_path.path_join(new_path)\n\tif new_path == old_path:\n\t\treturn\n\t\n\t%ConfigPath.text = new_path\n\t\n\tvar exists: bool\n\tif FileAccess.file_exists(new_path_abs):\n\t\texists = true\n\t\n\tvar error := OK\n\tvar old_empty: bool\n\tif exists:\n\t\told_empty = true\n\t\tfor value in Data.local_config.values():\n\t\t\tif value is String or value is Array:\n\t\t\t\tif not value.is_empty():\n\t\t\t\t\told_empty = false\n\t\t\t\t\tbreak\n\t\t\n\t\tif old_empty:\n\t\t\tOS.move_to_trash(Data.project_path.path_join(old_path))\n\telse:\n\t\terror = DirAccess.rename_absolute(Data.project_path.path_join(old_path), new_path_abs)\n\t\n\tif error == OK:\n\t\tvar settings_path := Data.project_path.path_join(\\\"project.godot\\\")\n\t\tvar settings := FileAccess.get_file_as_string(settings_path).split(\\\"\\\\n\\\")\n\t\tconst setting_line := \\\"_project_builder_config_path=\\\"\n\t\t\n\t\tvar replaced: bool\n\t\tfor i in settings.size():\n\t\t\tif settings[i].begins_with(setting_line):\n\t\t\t\tsettings[i] = \\\"%s\\\\\\\"%s\\\\\\\"\\\" % [setting_line, new_path]\n\t\t\t\treplaced = true\n\t\t\t\tbreak\n\t\t\n\t\tif not replaced:\n\t\t\tsettings.insert(10, \\\"%s\\\\\\\"%s\\\\\\\"\\\" % [setting_line, new_path])\n\t\t\n\t\tvar saver := FileAccess.open(settings_path, FileAccess.WRITE)\n\t\tif not saver:\n\t\t\tshow_error.call(\\\"Failed to save project settings, error %d.\\\" % FileAccess.get_open_error())\n\t\t\treturn\n\t\telse:\n\t\t\tsaver.store_string(\\\"\\\\n\\\".join(settings))\n\t\t\tsaver.close()\n\t\t\n\t\tData.local_config_file = new_path\n\t\t%ConfigPath.validate()\n\t\t\n\t\tif exists: # Reload config if changed to existing file.\n\t\t\t%PathError.dialog_text = \\\"Config file already exists in that directory.\\\\nIt will be used instead and the Project Builder will reload.\\\"\n\t\t\tif old_empty:\n\t\t\t\t%PathError.dialog_text += \\\"\\\\n\\\" + \\\"The old config file was empty, it will be deleted.\\\"\n\t\t\t%PathError.popup_centered()\n\t\t\t\n\t\t\tawait %PathError.visibility_changed\n\t\t\tData.load_project(Data.project_path)\n\t\t\tget_tree().reload_current_scene()\n\telse:\n\t\tshow_error.call(\\\"Failed to move config file, error %d.\\\" % error)\n\"\n\n[sub_resource type=\"StyleBoxEmpty\" id=\"StyleBoxEmpty_1o4l0\"]\ncontent_margin_left = 4.0\ncontent_margin_right = 4.0\n\n[sub_resource type=\"GDScript\" id=\"GDScript_fqp3h\"]\nscript/source = \"extends SpinBox\n\n# ಠ_ಠ\nvar text: int:\n\tset(v):\n\t\tvalue = v\n\tget:\n\t\treturn value\n\"\n\n[sub_resource type=\"GDScript\" id=\"GDScript_s2yn1\"]\nscript/source = \"extends Control\n\nfunc _ready() -> void:\n\tif Data.first_load:\n\t\tget_tree().create_timer(0.4).timeout.connect(queue_free)\n\telse:\n\t\tqueue_free()\n\"\n\n[node name=\"Main\" type=\"Control\"]\nlayout_mode = 3\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nscript = ExtResource(\"1_r1xkq\")\nmetadata/_edit_lock_ = true\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nmetadata/_edit_lock_ = true\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\ntheme_override_constants/margin_top = 8\ntheme_override_constants/margin_bottom = 8\n\n[node name=\"Title\" type=\"Label\" parent=\"VBoxContainer/MarginContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntheme_override_font_sizes/font_size = 30\ntext = \"Project Builder - %s\"\nhorizontal_alignment = 1\n\n[node name=\"TabContainer\" type=\"TabContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\nsize_flags_vertical = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_x81aq\")\ntab_alignment = 1\nscript = SubResource(\"GDScript_yepl2\")\n\n[node name=\"Routines\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nmetadata/_tab_index = 0\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer/TabContainer/Routines\"]\nlayout_mode = 2\ntheme_override_constants/margin_top = 8\ntheme_override_constants/margin_bottom = 8\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer/TabContainer/Routines/MarginContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Add Routine\"\n\n[node name=\"ScrollContainer\" type=\"ScrollContainer\" parent=\"VBoxContainer/TabContainer/Routines\"]\nlayout_mode = 2\nsize_flags_vertical = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_nuonq\")\n\n[node name=\"RoutineContainer\" type=\"HFlowContainer\" parent=\"VBoxContainer/TabContainer/Routines/ScrollContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nalignment = 1\n\n[node name=\"Control2\" type=\"Control\" parent=\"VBoxContainer/TabContainer/Routines\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Preset Templates\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer\"]\nvisible = false\nlayout_mode = 2\nmetadata/_tab_index = 1\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer/TabContainer/Preset Templates\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntheme_override_constants/margin_top = 8\ntheme_override_constants/margin_bottom = 8\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer/TabContainer/Preset Templates/MarginContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 4\nsize_flags_vertical = 4\ntext = \"Add Template\"\n\n[node name=\"MarginContainer2\" type=\"MarginContainer\" parent=\"VBoxContainer/TabContainer/Preset Templates\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\ntheme_override_constants/margin_left = 8\ntheme_override_constants/margin_right = 8\n\n[node name=\"TemplateScroll\" type=\"ScrollContainer\" parent=\"VBoxContainer/TabContainer/Preset Templates/MarginContainer2\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxFlat_nuonq\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Preset Templates/MarginContainer2/TemplateScroll\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"TemplateContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Preset Templates/MarginContainer2/TemplateScroll/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"Tasks\" type=\"MarginContainer\" parent=\"VBoxContainer/TabContainer\"]\nvisible = false\nlayout_mode = 2\ntheme_override_constants/margin_left = 8\ntheme_override_constants/margin_right = 8\nmetadata/_tab_index = 2\n\n[node name=\"ScrollContainer\" type=\"ScrollContainer\" parent=\"VBoxContainer/TabContainer/Tasks\"]\nlayout_mode = 2\nsize_flags_vertical = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxEmpty_cy4oc\")\n\n[node name=\"TaskContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Tasks/ScrollContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Config\" type=\"HBoxContainer\" parent=\"VBoxContainer/TabContainer\"]\nvisible = false\nlayout_mode = 2\ntheme = SubResource(\"Theme_v1gob\")\ntheme_override_constants/separation = 32\nscript = SubResource(\"GDScript_rk2qg\")\nmetadata/_tab_index = 3\n\n[node name=\"ScrollContainer\" type=\"ScrollContainer\" parent=\"VBoxContainer/TabContainer/Config\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxEmpty_1o4l0\")\nvertical_scroll_mode = 4\nscript = SubResource(\"GDScript_yepl2\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nalignment = 1\n\n[node name=\"Global\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nlayout_mode = 2\ntheme_type_variation = &\"HeaderMedium\"\ntext = \"Global Config\"\nhorizontal_alignment = 1\n\n[node name=\"Godot\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Godot\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot/VBoxContainer\"]\nlayout_mode = 2\ntext = \"Godot Path\"\nhorizontal_alignment = 1\n\n[node name=\"GlobalGodot\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\n\n[node name=\"Steam\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Steam\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label3\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\"]\nlayout_mode = 2\ntext = \"Steam CMD Path\"\nhorizontal_alignment = 1\n\n[node name=\"SteamCMD\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\n\n[node name=\"Label4\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\"]\nlayout_mode = 2\ntext = \"Username\"\nhorizontal_alignment = 1\n\n[node name=\"SteamUsername\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label5\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\"]\nlayout_mode = 2\ntext = \"Password\"\nhorizontal_alignment = 1\n\n[node name=\"SteamPassword\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\nsecret = true\n\n[node name=\"GOG\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"GOG\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label3\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Pipeline Builder Path\"\nhorizontal_alignment = 1\n\n[node name=\"PipelineBuilderPath\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\n\n[node name=\"Label4\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Username\"\nhorizontal_alignment = 1\n\n[node name=\"GOGUsername\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label5\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Password\"\nhorizontal_alignment = 1\n\n[node name=\"GOGPassword\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\nsecret = true\n\n[node name=\"Epic\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Epic\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label3\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Build Patch Tool Path\"\nhorizontal_alignment = 1\n\n[node name=\"BuildPatchToolPath\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\n\n[node name=\"Label4\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Organization ID\"\nhorizontal_alignment = 1\n\n[node name=\"EpicOrganizationID\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label5\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Client ID\"\nhorizontal_alignment = 1\n\n[node name=\"EpicClientID\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label6\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Client Secret\"\nhorizontal_alignment = 1\n\n[node name=\"EpicClientSecret\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\nsecret = true\n\n[node name=\"Label7\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Client Secret Env Variable\"\nhorizontal_alignment = 1\n\n[node name=\"EpicClientSecretEnvVar\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Itch\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Itch\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label10\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Butler Path\"\nhorizontal_alignment = 1\n\n[node name=\"ItchButlerPath\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\n\n[node name=\"Label11\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Username\"\nhorizontal_alignment = 1\n\n[node name=\"ItchUsername\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"HSeparator\" type=\"HSeparator\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer\"]\nlayout_mode = 2\nalignment = 1\n\n[node name=\"Label\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/HBoxContainer\"]\nlayout_mode = 2\ntext = \"Execution Delay\"\n\n[node name=\"ExecutionDelay\" type=\"SpinBox\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nvalue = 2.0\nsuffix = \"s\"\nscript = SubResource(\"GDScript_fqp3h\")\n\n[node name=\"ScrollContainer2\" type=\"ScrollContainer\" parent=\"VBoxContainer/TabContainer/Config\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\ntheme_override_styles/panel = SubResource(\"StyleBoxEmpty_1o4l0\")\nvertical_scroll_mode = 4\nscript = SubResource(\"GDScript_yepl2\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\nalignment = 1\n\n[node name=\"Local\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nlayout_mode = 2\ntheme_type_variation = &\"HeaderMedium\"\ntext = \"Local Config\"\nhorizontal_alignment = 1\n\n[node name=\"Godot\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Godot\"\ntitle_alignment = 1\n\n[node name=\"Godot\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label2\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot\"]\nlayout_mode = 2\ntext = \"Project Builder Configuration Path\"\nhorizontal_alignment = 1\n\n[node name=\"ConfigPath\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nlayout_mode = 2\nmode = 4\nscope = 1\nmissing_mode = 1\n\n[node name=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath\" index=\"0\"]\neditable = false\n\n[node name=\"PathError\" type=\"AcceptDialog\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath\"]\nunique_name_in_owner = true\ntitle = \"Error!\"\n\n[node name=\"Label3\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot\"]\nlayout_mode = 2\ntext = \"Godot Exec For This Project\"\nhorizontal_alignment = 1\n\n[node name=\"LocalGodot\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nlayout_mode = 2\nmode = 1\n\n[node name=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/LocalGodot\" index=\"0\"]\nplaceholder_text = \"Leave empty to use global path.\"\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Epic\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Epic\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label9\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Product ID\"\nhorizontal_alignment = 1\n\n[node name=\"EpicProductID\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label10\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Artifact ID\"\nhorizontal_alignment = 1\n\n[node name=\"EpicArtifactID\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label11\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Cloud Directory\"\nhorizontal_alignment = 1\n\n[node name=\"EpicCloudDir\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nscope = 1\nmissing_mode = 2\n\n[node name=\"Itch\" type=\"FoldableContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nauto_translate_mode = 2\nlayout_mode = 2\ntitle = \"Itch\"\ntitle_alignment = 1\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Label8\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Game Name\"\nhorizontal_alignment = 1\n\n[node name=\"ItchGameName\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label9\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Default Channel\"\nhorizontal_alignment = 1\n\n[node name=\"ItchDefaultChannel\" type=\"LineEdit\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\ncaret_blink = true\ncaret_blink_interval = 0.5\n\n[node name=\"Label10\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Version File\"\nhorizontal_alignment = 1\n\n[node name=\"ItchVersionFile\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer\" instance=ExtResource(\"2_ood4h\")]\nunique_name_in_owner = true\nlayout_mode = 2\nmode = 1\nscope = 1\nfilters = PackedStringArray(\"*.txt\")\n\n[node name=\"HSeparator\" type=\"HSeparator\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"Button\" type=\"Button\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Run Project Scan\"\n\n[node name=\"ScanFinished\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Button\"]\nunique_name_in_owner = true\nvisible = false\nlayout_mode = 1\nanchors_preset = 9\nanchor_bottom = 1.0\noffset_left = 142.0\noffset_right = 249.0\ngrow_vertical = 2\ntext = \"Scan finished!\"\nvertical_alignment = 1\n\n[node name=\"Control\" type=\"Control\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\ncustom_minimum_size = Vector2(0, 8)\nlayout_mode = 2\n\n[node name=\"PluginStatus\" type=\"Label\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntext = \"Plugin Status\"\nhorizontal_alignment = 1\n\n[node name=\"InstallPlugin\" type=\"Button\" parent=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Instal Project Builder Plugin\"\n\n[node name=\"Back\" type=\"Button\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 0\nshortcut = ExtResource(\"4_bgxb8\")\nshortcut_in_tooltip = false\ntext = \"Back\"\nicon = ExtResource(\"2_fue2w\")\n\n[node name=\"OpenLogs\" type=\"Button\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 1\nanchors_preset = 1\nanchor_left = 1.0\nanchor_right = 1.0\noffset_left = -57.0\noffset_bottom = 31.0\ngrow_horizontal = 0\ntext = \"Open Logs Folder\"\nicon = ExtResource(\"6_kgk8y\")\n\n[node name=\"InputKiller\" type=\"Control\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nscript = SubResource(\"GDScript_s2yn1\")\nmetadata/_edit_lock_ = true\n\n[connection signal=\"tab_changed\" from=\"VBoxContainer/TabContainer\" to=\".\" method=\"tab_changed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/TabContainer/Routines/MarginContainer/Button\" to=\".\" method=\"_add_routine_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/TabContainer/Preset Templates/MarginContainer/Button\" to=\".\" method=\"_add_template_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Button\" to=\".\" method=\"run_project_scan\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/InstallPlugin\" to=\"VBoxContainer/TabContainer/Config\" method=\"_on_install_plugin_pressed\"]\n[connection signal=\"pressed\" from=\"Back\" to=\".\" method=\"go_back\"]\n[connection signal=\"pressed\" from=\"OpenLogs\" to=\".\" method=\"open_logs\"]\n\n[editable path=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath\"]\n[editable path=\"VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/LocalGodot\"]\n"
  },
  {
    "path": "Scenes/ProjectManager.gd",
    "content": "extends Control\n\n@onready var custom_list: HBoxContainer = %CustomList\n@onready var projects: VBoxContainer = %Projects\n@onready var over: Label = %Over\n\nvar user_arguments := OS.get_cmdline_user_args()\n\nfunc _ready() -> void:\n\tvar i := user_arguments.find(\"--open-project\")\n\tif i > -1:\n\t\tif user_arguments.size() < i + 2:\n\t\t\tprinterr(\"No project path provided with --open-project.\")\n\t\telse:\n\t\t\tvar project_path := user_arguments[i + 1]\n\t\t\tif DirAccess.dir_exists_absolute(project_path):\n\t\t\t\tData.from_plugin = true\n\t\t\t\tload_project.call_deferred(project_path)\n\t\t\t\treturn\n\t\t\telse:\n\t\t\t\tprinterr(\"The project provided for --open-project does not exist.\")\n\t\n\ti = user_arguments.find(\"--execute-routine\")\n\tif i > -1:\n\t\tprinterr(\"--execute-routine was provided, but no project was opened with --open-project.\")\n\t\tget_tree().quit(1)\n\t\treturn\n\t\n\ti = user_arguments.find(\"--exit\")\n\tif i > -1:\n\t\tprinterr(\"--exit argument provided, but no --execute-routine. It will be ignored.\")\n\t\n\tcustom_list.text = Data.global_config[\"custom_project_list\"]\n\tload_project_list()\n\nfunc load_project_list():\n\tfor project in projects.get_children():\n\t\tproject.queue_free()\n\t\n\tvar projects_path: String = custom_list.text\n\t\n\tvar i := user_arguments.find(\"--projects-file-path\")\n\tif i > -1:\n\t\tif user_arguments.size() < i + 2:\n\t\t\tpush_warning(\"--projects-file-path -- Missing projects file path.\")\n\t\telse:\n\t\t\tover.show()\n\t\t\tprojects_path = user_arguments[i + 1]\n\t\n\tif not FileAccess.file_exists(projects_path):\n\t\tvar editor_data := OS.get_user_data_dir().get_base_dir().get_base_dir()\n\t\tprojects_path = editor_data.path_join(\"projects.cfg\")\n\t\n\tvar project_list := ConfigFile.new()\n\tif project_list.load(projects_path) != OK:\n\t\treturn\n\t\n\tfor project in project_list.get_sections():\n\t\tvar project_entry := preload(\"res://Nodes/ProjectEntry.tscn\").instantiate()\n\t\tprojects.add_child(project_entry)\n\t\tproject_entry.set_project(project, load_project)\n\nfunc load_project(project: String):\n\tData.load_project(project)\n\t\n\tvar i := user_arguments.find(\"--execute-routine\")\n\tif i > -1:\n\t\tvar j := user_arguments.find(\"--exit\")\n\t\tif j > -1:\n\t\t\tData.auto_exit = true\n\t\t\n\t\tif user_arguments.size() < i + 2:\n\t\t\tprint(\"No routine name provided for --execute-routine.\")\n\t\t\tprint_routines_and_exit()\n\t\t\treturn\n\t\telse:\n\t\t\tvar routine_name := user_arguments[i + 1]\n\t\t\tfor routine in Data.routines:\n\t\t\t\tif routine[\"name\"] == routine_name:\n\t\t\t\t\tData.current_routine = routine\n\t\t\t\t\tget_tree().change_scene_to_file(\"res://Scenes/Execution.tscn\")\n\t\t\t\t\treturn\n\t\t\t\n\t\t\tprinterr(\"The routine provided for --execute-routine does not exist.\")\n\t\t\tprint_routines_and_exit()\n\t\t\treturn\n\t\n\ti = user_arguments.find(\"--exit\")\n\tif i > -1:\n\t\tprinterr(\"--exit argument provided, but no --execute-routine. It will be ignored.\")\n\t\n\tget_tree().change_scene_to_packed(Data.main)\n\nfunc print_routines_and_exit():\n\tprint(\"Available routines:\")\n\tfor routine in Data.routines:\n\t\tprint(routine[\"name\"])\n\t\n\tif Data.auto_exit:\n\t\tget_tree().quit(1)\n\nfunc project_list_path_changed() -> void:\n\tData.global_config[\"custom_project_list\"] = custom_list.text\n\tData.save_global_config()\n\tload_project_list()\n"
  },
  {
    "path": "Scenes/ProjectManager.gd.uid",
    "content": "uid://drooidtlec0og\n"
  },
  {
    "path": "Scenes/ProjectManager.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://eu3eioilmxkb\"]\n\n[ext_resource type=\"Script\" uid=\"uid://drooidtlec0og\" path=\"res://Scenes/ProjectManager.gd\" id=\"1_edpqn\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"2_8a1pu\"]\n\n[node name=\"ProjectManager\" type=\"ScrollContainer\"]\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nscript = ExtResource(\"1_edpqn\")\nmetadata/_edit_lock_ = true\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nalignment = 1\n\n[node name=\"Label\" type=\"Label\" parent=\"VBoxContainer/HBoxContainer\"]\nlayout_mode = 2\ntext = \"Custom Project List\"\n\n[node name=\"CustomList\" parent=\"VBoxContainer/HBoxContainer\" instance=ExtResource(\"2_8a1pu\")]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(250, 0)\nlayout_mode = 2\nmode = 1\nmissing_mode = 2\nfilters = PackedStringArray(\"*.cfg\")\nempty_is_valid = true\n\n[node name=\"Over\" type=\"Label\" parent=\"VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nvisible = false\nmodulate = Color(1, 1, 0, 1)\nlayout_mode = 2\ntext = \"Overridden by cmd argument.\"\n\n[node name=\"Projects\" type=\"VBoxContainer\" parent=\"VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 6\nsize_flags_vertical = 3\n\n[connection signal=\"path_changed\" from=\"VBoxContainer/HBoxContainer/CustomList\" to=\".\" method=\"project_list_path_changed\"]\n"
  },
  {
    "path": "Scenes/RoutineBuilder.gd",
    "content": "extends Control\n\n@onready var task_list: VBoxContainer = %TaskList\n@onready var add_task: MenuButton = %AddTask\n\nvar routine: Dictionary\nvar task_to_test: Task\nvar discard: bool\n\nfunc _ready() -> void:\n\troutine = Data.current_routine\n\t%RoutineName.text = routine[\"name\"]\n\t%OnFail.selected = routine[\"on_fail\"]\n\t\n\tfor task in Data.tasks.values():\n\t\tadd_task.get_popup().add_item(task[\"name\"])\n\t\tadd_task.get_popup().set_item_metadata(-1, task[\"scene\"])\n\t\n\tfor task: Dictionary in routine[\"tasks\"]:\n\t\tvar task_instance := create_task(task[\"scene\"])\n\t\ttask_instance.load_data(task[\"data\"])\n\t\n\tadd_task.get_popup().index_pressed.connect(_create_task)\n\tupdate_paste()\n\tvalidate_routine_name()\n\nfunc _create_task(idx: int):\n\tcreate_task(add_task.get_popup().get_item_metadata(idx))\n\nfunc create_task(scene: String) -> Task:\n\tvar container := preload(\"res://Nodes/TaskContainer.tscn\").instantiate()\n\ttask_list.add_child(container)\n\tcontainer.copied.connect(update_paste)\n\tcontainer.owner = self\n\t\n\tvar task: Task = container.set_task_scene(scene)\n\ttask.owner = self\n\ttask._initialize()\n\treturn task\n\nfunc _back_pressed() -> void:\n\tget_tree().change_scene_to_packed(Data.main)\n\nfunc _discard_pressed() -> void:\n\tdiscard = true\n\tget_tree().auto_accept_quit = true\n\tget_tree().change_scene_to_packed(Data.main)\n\nfunc test_task(task: Task):\n\ttask_to_test = task\n\tget_tree().current_scene = null\n\tget_parent().remove_child(self)\n\nfunc _exit_tree() -> void:\n\tData.reset_current_routine()\n\t\n\tif discard:\n\t\treturn\n\t\n\tvar routine_tasks: Array[Dictionary]\n\tvar test_data: Dictionary\n\t\n\tfor task: Task in task_list.get_children().map(func(container: Node) -> Task: return container.task):\n\t\tvar task_data := Dictionary()\n\t\ttask_data[\"scene\"] = task.scene_file_path.get_file().get_basename()\n\t\ttask_data[\"data\"] = task.store_data()\n\t\troutine_tasks.append(task_data)\n\t\t\n\t\tif task == task_to_test:\n\t\t\ttest_data = task_data\n\t\n\troutine[\"name\"] = %RoutineName.text\n\troutine[\"tasks\"] = routine_tasks\n\troutine[\"on_fail\"] = %OnFail.selected\n\tData.queue_save_local_config()\n\t\n\tif task_to_test:\n\t\tData.current_routine = { \"name\": \"Test\", \"tasks\": [ test_data ], \"on_fail\": 0 }\n\t\tqueue_free()\n\t\tget_tree().change_scene_to_file(\"res://Scenes/Execution.tscn\")\n\t\nfunc paste_task() -> void:\n\tvar task := Data.copied_task\n\tif task.is_empty():\n\t\treturn\n\t\n\tvar task_instance := create_task(task[\"scene\"])\n\ttask_instance.load_data(task[\"data\"])\n\nfunc update_paste():\n\t%PasteTask.disabled = Data.copied_task.is_empty()\n\nfunc validate_routine_name():\n\tvar valid := true\n\tfor rout in Data.routines:\n\t\tif rout != routine and rout[\"name\"] == %RoutineName.text:\n\t\t\tvalid = false\n\t\t\tbreak\n\t\n\tif valid:\n\t\tget_tree().auto_accept_quit = true\n\t\t%RoutineName.modulate = Color.WHITE\n\t\t%Back.disabled = false\n\telse:\n\t\tget_tree().auto_accept_quit = false\n\t\t%RoutineName.modulate = Color.RED\n\t\t%Back.disabled = true\n"
  },
  {
    "path": "Scenes/RoutineBuilder.gd.uid",
    "content": "uid://bw6lvvotkx7nc\n"
  },
  {
    "path": "Scenes/RoutineBuilder.tscn",
    "content": "[gd_scene load_steps=5 format=3 uid=\"uid://dbb72q773nirj\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bw6lvvotkx7nc\" path=\"res://Scenes/RoutineBuilder.gd\" id=\"1_gekas\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://dt6drd8cw1dhl\" path=\"res://Icons/Back.svg\" id=\"2_0gvmh\"]\n[ext_resource type=\"Shortcut\" uid=\"uid://d0olqx0bjlhp1\" path=\"res://Nodes/GUI/ExitShortcut.tres\" id=\"2_ghd1v\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://3i2umr3kmry6\" path=\"res://Icons/Paste.svg\" id=\"3_c5oe2\"]\n\n[node name=\"RoutineBuilder\" type=\"VBoxContainer\"]\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nscript = ExtResource(\"1_gekas\")\nmetadata/_edit_lock_ = true\n\n[node name=\"HBoxContainer2\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"Back\" type=\"Button\" parent=\"HBoxContainer2\"]\nunique_name_in_owner = true\nlayout_mode = 2\nshortcut = ExtResource(\"2_ghd1v\")\nshortcut_in_tooltip = false\ntext = \"Back\"\nicon = ExtResource(\"2_0gvmh\")\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"HBoxContainer2\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nalignment = 1\n\n[node name=\"Label\" type=\"Label\" parent=\"HBoxContainer2/HBoxContainer\"]\nlayout_mode = 2\ntext = \"Routine Name\"\n\n[node name=\"RoutineName\" type=\"LineEdit\" parent=\"HBoxContainer2/HBoxContainer\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(300, 0)\nlayout_mode = 2\ntheme_override_constants/minimum_character_width = 24\nmax_length = 24\n\n[node name=\"Back2\" type=\"Button\" parent=\"HBoxContainer2\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nmodulate = Color(1, 0, 0, 1)\nlayout_mode = 2\ntext = \"Discard Changes\"\nicon = ExtResource(\"2_0gvmh\")\nicon_alignment = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 2\nalignment = 1\n\n[node name=\"Label2\" type=\"Label\" parent=\"HBoxContainer\"]\nlayout_mode = 2\ntext = \"On Fail:\"\n\n[node name=\"OnFail\" type=\"OptionButton\" parent=\"HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nselected = 0\nitem_count = 2\npopup/item_0/text = \"Abort\"\npopup/item_0/id = 1\npopup/item_1/text = \"Continue\"\npopup/item_1/id = 0\n\n[node name=\"ScrollContainer\" type=\"ScrollContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_vertical = 3\nmetadata/_edit_lock_ = true\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"ScrollContainer\"]\ncustom_minimum_size = Vector2(800, 0)\nlayout_mode = 2\nsize_flags_horizontal = 6\nsize_flags_vertical = 3\nmetadata/_edit_lock_ = true\n\n[node name=\"TaskList\" type=\"VBoxContainer\" parent=\"ScrollContainer/VBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"ScrollContainer/VBoxContainer\"]\nlayout_mode = 2\nalignment = 1\n\n[node name=\"AddTask\" type=\"MenuButton\" parent=\"ScrollContainer/VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Add Task\"\nflat = false\n\n[node name=\"PasteTask\" type=\"Button\" parent=\"ScrollContainer/VBoxContainer/HBoxContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Paste Task\"\nicon = ExtResource(\"3_c5oe2\")\n\n[connection signal=\"pressed\" from=\"HBoxContainer2/Back\" to=\".\" method=\"_back_pressed\"]\n[connection signal=\"text_changed\" from=\"HBoxContainer2/HBoxContainer/RoutineName\" to=\".\" method=\"validate_routine_name\" unbinds=1]\n[connection signal=\"pressed\" from=\"HBoxContainer2/Back2\" to=\".\" method=\"_discard_pressed\"]\n[connection signal=\"pressed\" from=\"ScrollContainer/VBoxContainer/HBoxContainer/PasteTask\" to=\".\" method=\"paste_task\"]\n"
  },
  {
    "path": "Scripts/Data.gd",
    "content": "extends Node\n\nconst CONFIG_FILE = \"project_builds_config.txt\"\nvar local_config_file: String\n\nvar global_config: Dictionary\nvar local_config: Dictionary\nvar project_path: String\nvar from_plugin: bool\nvar auto_exit: bool\n\nvar first_load: bool\nvar initial_load: bool\nvar static_initialize_tasks: Array[Script]\nvar sensitive_settings: Array[String]\n\nvar tasks: Dictionary#[String, Dictionary]\nvar routines: Array[Dictionary]\nvar templates: Array[Dictionary]\n\nvar current_routine: Dictionary\nvar copied_task: Dictionary\n\nvar save_local_timer: Timer\nvar save_global_timer: Timer\n\nvar main: PackedScene\n\nfunc _init() -> void:\n\tvar task_path := get_res_path().path_join(\"Tasks\")\n\tfor task in DirAccess.get_files_at(task_path):\n\t\tif task.get_extension() == \"tscn\":\n\t\t\tregister_task(task_path.path_join(task))\n\t\n\tvar global_defaults = {\"project_builder_path\": \"\", \"project_builder_executable\": \"\", \"custom_project_list\": \"\"}\n\t\n\tvar global_config_file := FileAccess.open(\"user://\".path_join(CONFIG_FILE), FileAccess.READ)\n\tif global_config_file:\n\t\tglobal_config = str_to_var(global_config_file.get_as_text())\n\t\tglobal_config.merge(global_defaults)\n\telse:\n\t\tglobal_config = global_defaults\n\t\n\tsave_local_timer = Timer.new()\n\tsave_local_timer.wait_time = 0.5\n\tsave_local_timer.one_shot = true\n\tadd_child(save_local_timer)\n\t\n\tsave_global_timer = save_local_timer.duplicate()\n\tadd_child(save_global_timer)\n\t\n\tsave_local_timer.timeout.connect(save_local_config)\n\tsave_global_timer.timeout.connect(save_global_config)\n\t\n\tmain = load(\"res://Scenes/Main.tscn\")\n\nfunc _ready() -> void:\n\tif not OS.get_cmdline_user_args().is_empty():\n\t\treturn\n\t\n\tvar path := ProjectSettings.globalize_path(\"res://\")\n\tif path != global_config[\"project_builder_path\"]:\n\t\tglobal_config[\"project_builder_path\"] = path\n\t\tqueue_save_global_config()\n\t\n\tpath = OS.get_executable_path()\n\tif path != global_config[\"project_builder_executable\"]:\n\t\tglobal_config[\"project_builder_executable\"] = path\n\t\tqueue_save_global_config()\n\nfunc get_project_config_path(project: String, config_file: ConfigFile = null) -> String:\n\tif not config_file:\n\t\tconfig_file = ConfigFile.new()\n\t\tconfig_file.load(project)\n\t\n\tvar config_path: String = config_file.get_value(\"addons\", \"project_builder/config_path\", \"\") # compat\n\tif config_path.is_empty():\n\t\tconfig_path = config_file.get_value(\"\", \"_project_builder_config_path\", CONFIG_FILE)\n\t\n\treturn config_path.trim_prefix(\"res://\")\n\nfunc load_project(path: String):\n\tproject_path = path\n\tfirst_load = true\n\t\n\tlocal_config_file = get_project_config_path(project_path.path_join(\"project.godot\"))\n\tvar fa := FileAccess.open(project_path.path_join(local_config_file), FileAccess.READ)\n\tif fa:\n\t\tlocal_config = str_to_var(fa.get_as_text())\n\telse:\n\t\tlocal_config = {}\n\t\tlocal_config[\"routines\"]  = Array([], TYPE_DICTIONARY, &\"\", null)\n\t\tlocal_config[\"templates\"]  = Array([], TYPE_DICTIONARY, &\"\", null)\n\t\tinitial_load = true\n\n\troutines = local_config[\"routines\"]\n\ttemplates = local_config[\"templates\"]\n\nfunc register_task(scene: String):\n\tvar data := Dictionary()\n\tvar scene_base := scene.get_file().get_basename()\n\tdata[\"scene\"] = scene_base\n\tdata[\"scene_cache\"] = load(scene)\n\t\n\tvar instance: Task = load(scene).instantiate()\n\tdata[\"name\"] = instance._get_task_name()\n\tif instance.has_static_configuration:\n\t\tstatic_initialize_tasks.append(instance.get_script())\n\tinstance.free()\n\t\n\ttasks[scene_base] = data\n\nfunc create_routine() -> Dictionary:\n\tvar routine := Dictionary()\n\troutine[\"name\"] = get_unique_name(routines, \"New Routine\", \"%d\")\n\troutine[\"on_fail\"] = 0\n\troutine[\"tasks\"] = []\n\troutines.append(routine)\n\treturn routine\n\nfunc reset_current_routine() -> void:\n\tcurrent_routine = {}\n\nfunc get_template(template_name: String) -> Dictionary:\n\tfor template in templates:\n\t\tif template[\"name\"] == template_name:\n\t\t\treturn template\n\treturn {}\n\nfunc get_project_version() -> String:\n\tvar project := ConfigFile.new()\n\tproject.load(project_path.path_join(\"project.godot\"))\n\treturn project.get_value(\"application\", \"config/version\", \"\")\n\nfunc get_godot_path() -> String:\n\tvar local_godot: String = local_config[\"godot_path\"]\n\tif local_godot.is_empty():\n\t\treturn global_config[\"godot_path\"]\n\telse:\n\t\treturn local_godot\n\nfunc get_res_path() -> String:\n\tif OS.has_feature(\"editor\"):\n\t\treturn ProjectSettings.globalize_path(\"res://\")\n\telse:\n\t\treturn OS.get_executable_path().get_base_dir()\n\nfunc resolve_path(path: String) -> String:\n\tif path.is_absolute_path():\n\t\treturn path\n\telse:\n\t\treturn Data.project_path.path_join(path)\n\nfunc get_unique_name(dataset: Array[Dictionary], base: String, format_suffix: String, initial_count := 1) -> String:\n\tvar unique_name := base\n\tvar format_base := base + \" \" + format_suffix\n\tvar tries := initial_count\n\t\n\twhile dataset.any(func(data: Dictionary) -> bool: return data[\"name\"] == unique_name):\n\t\ttries += 1\n\t\tunique_name = format_base % tries\n\t\n\treturn unique_name\n\nfunc queue_save_local_config():\n\tsave_local_timer.start()\n\nfunc save_local_config():\n\tsave_local_timer.stop()\n\tvar fa := FileAccess.open(project_path.path_join(local_config_file), FileAccess.WRITE)\n\tfa.store_string(var_to_str(local_config))\n\nfunc queue_save_global_config():\n\tsave_global_timer.start()\n\nfunc save_global_config():\n\tsave_global_timer.stop()\n\tvar fa := FileAccess.open(\"user://\".path_join(CONFIG_FILE), FileAccess.WRITE)\n\tfa.store_string(var_to_str(global_config))\n\nfunc _exit_tree() -> void:\n\tif not project_path.is_empty():\n\t\tsave_local_config()\n\tsave_global_config()\n"
  },
  {
    "path": "Scripts/Data.gd.uid",
    "content": "uid://cxigukxxvepyb\n"
  },
  {
    "path": "Scripts/Templates/Task/EmptyTask.gd",
    "content": "extends Task\n\nfunc _get_task_name() -> String:\n\treturn \"Empty Task\"\n\nfunc _get_execute_string() -> String:\n\treturn _get_task_name()\n\nstatic func _initialize_project() -> void:\n\tpass\n\nstatic func _begin_project_scan() -> void:\n\tpass\n\nstatic func _process_file(path: String) -> void:\n\tpass\n\nstatic func _end_project_scan() -> void:\n\tpass\n\nfunc _initialize() -> void:\n\tpass\n\nfunc _prevalidate() -> bool:\n\treturn true\n\nfunc _validate() -> bool:\n\treturn true\n\nfunc _get_command() -> String:\n\treturn \"\"\n\nfunc _get_arguments() -> PackedStringArray:\n\treturn []\n\nfunc _prepare() -> void:\n\tpass\n\nfunc _cleanup() -> void:\n\tpass\n\nfunc _load() -> void:\n\tpass\n\nfunc _store() -> void:\n\tpass\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\"Task description\",\n\t\t\"Argument Name|Description\",\n\t]\n"
  },
  {
    "path": "Scripts/Templates/Task/EmptyTask.gd.uid",
    "content": "uid://05ho57r0n7x\n"
  },
  {
    "path": "Tasks/ClearDirectory.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://baipfhiy5un08\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_63381\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_getv1\"]\nscript/source = \"extends \\\"ScriptTask.gd\\\"\n\n@onready var target_directory: HBoxContainer = %TargetDirectory\n@onready var file_filter: LineEdit = %FileFilter\n@onready var file_filter2: LineEdit = %FileFilter2\n\nfunc _get_task_name() -> String:\n\treturn \\\"Clear Directory Files\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Clear Files at %s\\\" % target_directory.text\n\nfunc _initialize() -> void:\n\tdefaults[\\\"target_directory\\\"] = \\\"\\\"\n\tdefaults[\\\"include_files\\\"] = \\\"\\\"\n\tdefaults[\\\"exclude_files\\\"] = \\\"\\\"\n\nfunc _validate() -> bool:\n\tif not DirAccess.dir_exists_absolute(Data.resolve_path(target_directory.text)):\n\t\terror_message = \\\"Target directory does not exist.\\\"\n\t\treturn false\n\t\n\treturn true\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret := super()\n\t\n\tret.append(Data.resolve_path(target_directory.text))\n\t\n\tif not file_filter.text.is_empty():\n\t\tret.append(\\\"--include\\\")\n\t\tret.append_array(file_filter.text.split(\\\" \\\"))\n\tif not file_filter2.text.is_empty():\n\t\tret.append(\\\"--exclude\\\")\n\t\tret.append_array(file_filter2.text.split(\\\" \\\"))\n\t\n\treturn ret\n\nfunc _load() -> void:\n\ttarget_directory.text = data[\\\"target_directory\\\"]\n\tfile_filter.text = data[\\\"include_files\\\"]\n\tfile_filter2.text = data[\\\"exclude_files\\\"]\n\nfunc _store() -> void:\n\tdata[\\\"target_directory\\\"] = target_directory.text\n\tdata[\\\"include_files\\\"] = file_filter.text\n\tdata[\\\"exclude_files\\\"] = file_filter2.text\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Clears all files in a directory. The files are not deleted, but moved to a special Trash directory in Project Builder's user data. The operation is not recursive.\\\",\n\t\t\\\"Target Directory|The directory to be cleared.\\\",\n\t\t\\\"Include Files|List of filters for files from the directory. Example: \\\\\\\"*.exe *.pck\\\\\\\". If empty, all files will be packed. Otherwise only files that match the filter will be included.\\\",\n\t\t\\\"Exclude Files|Filters files from the directory. Files that match this filter will be excluded.\\\",\n\t]\n\"\n\n[node name=\"ClearDirectory\" type=\"GridContainer\"]\noffset_right = 339.0\noffset_bottom = 31.0\ncolumns = 2\nscript = SubResource(\"GDScript_getv1\")\nscript_name = \"ClearDirectory.gd\"\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Target Directory\"\nhorizontal_alignment = 2\n\n[node name=\"TargetDirectory\" parent=\".\" instance=ExtResource(\"1_63381\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nscope = 1\nmissing_mode = 1\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Include Files\"\nhorizontal_alignment = 2\n\n[node name=\"FileFilter\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nplaceholder_text = \"Space-separated list of filters, e.g. \\\"*.exe\\\". Leave empty to include all.\"\n\n[node name=\"Label4\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Exclude Files\"\nhorizontal_alignment = 2\n\n[node name=\"FileFilter2\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\nplaceholder_text = \"Space-separated list of filters. Leave empty to exclude none.\"\n"
  },
  {
    "path": "Tasks/CopyFiles.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://bfxyekjccdafx\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_nbxxd\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_6ruue\"]\nscript/source = \"extends \\\"ScriptTask.gd\\\"\n\n@onready var source_path: Control = %SourcePath\n@onready var target_path: Control = %TargetPath\n@onready var recursive: CheckBox = %Recursive\n\nvar directory_mode: bool\n\nfunc _get_task_name() -> String:\n\treturn \\\"Copy File(s)\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Copy %s to %s\\\" % [source_path.text, target_path.text]\n\nfunc _initialize() -> void:\n\tdefaults[\\\"source_path\\\"] = \\\"\\\"\n\tdefaults[\\\"target_path\\\"] = \\\"\\\"\n\tdefaults[\\\"recursive\\\"] = true\n\nfunc _prevalidate() -> bool:\n\tif source_path.text == target_path.text:\n\t\terror_message = \\\"Source and target paths are the same.\\\"\n\t\treturn false\n\telse:\n\t\treturn true\n\nfunc _validate() -> bool:\n\tif DirAccess.dir_exists_absolute(Data.resolve_path(source_path.text)):\n\t\tdirectory_mode = true\n\telif FileAccess.file_exists(Data.resolve_path(source_path.text)):\n\t\tdirectory_mode = false\n\telse:\n\t\terror_message = \\\"Source path does not point to any file nor directory.\\\"\n\t\n\treturn error_message.is_empty()\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret := super()\n\t\n\tret.append(Data.resolve_path(source_path.text))\n\tret.append(Data.resolve_path(target_path.text))\n\tif directory_mode:\n\t\tif recursive.button_pressed:\n\t\t\tret.append(\\\"dir_recursive\\\")\n\t\telse:\n\t\t\tret.append(\\\"dir\\\")\n\telse:\n\t\tret.append(\\\"file\\\")\n\t\n\treturn ret\n\nfunc _load() -> void:\n\tsource_path.text = data[\\\"source_path\\\"]\n\ttarget_path.text = data[\\\"target_path\\\"]\n\trecursive.button_pressed = data[\\\"recursive\\\"]\n\nfunc _store() -> void:\n\tdata[\\\"source_path\\\"] = source_path.text\n\tdata[\\\"target_path\\\"] = target_path.text\n\tdata[\\\"recursive\\\"] = recursive.button_pressed\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Copies the specified files or directories to a new location.\\\",\n\t\t\\\"Source Path|File or directory path which is going to be copied.\\\",\n\t\t\\\"Target Path|Target file/directory path where the files will be copied to..\\\",\n\t\t\\\"Recursive|If source path is a directory, this option enables copying sub-directories.\\\",\n\t]\n\"\n\n[node name=\"CopyFiles\" type=\"GridContainer\"]\noffset_right = 400.0\noffset_bottom = 101.0\ncolumns = 2\nscript = SubResource(\"GDScript_6ruue\")\nscript_name = \"CopyFiles.gd\"\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Source Path\"\nhorizontal_alignment = 2\n\n[node name=\"SourcePath\" parent=\".\" instance=ExtResource(\"1_nbxxd\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nmode = 3\nscope = 1\nmissing_mode = 1\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Target Path\"\nhorizontal_alignment = 2\n\n[node name=\"TargetPath\" parent=\".\" instance=ExtResource(\"1_nbxxd\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\nmode = 3\nscope = 1\n\n[node name=\"Control\" type=\"Control\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 8\n\n[node name=\"Recursive\" type=\"CheckBox\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 0\nbutton_pressed = true\ntext = \"Recursive\"\n"
  },
  {
    "path": "Tasks/CustomTask.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://2g67bchptwsm\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://bfbht01onlf1a\" path=\"res://Nodes/GUI/StringContainer.tscn\" id=\"1_siigy\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_3vtsj\"]\nscript/source = \"extends Task\n\n@onready var command: LineEdit = %Command\n@onready var arguments: Control = %Arguments\n\nfunc _get_task_name() -> String:\n\treturn \\\"Custom Task\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Custom Task: %s\\\" % _get_command()\n\nfunc _initialize() -> void:\n\tdefaults[\\\"command\\\"] = \\\"\\\"\n\tdefaults[\\\"arguments\\\"] = PackedStringArray()\n\nfunc _get_command() -> String:\n\treturn process_string(command.text)\n\nfunc _get_arguments() -> PackedStringArray:\n\treturn Array(arguments.get_strings()).map(process_string)\n\nfunc _load() -> void:\n\tcommand.text = data[\\\"command\\\"]\n\targuments.set_strings(data[\\\"arguments\\\"])\n\nfunc _store() -> void:\n\tdata[\\\"command\\\"] = command.text\n\tdata[\\\"arguments\\\"] = arguments.get_strings()\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Executes a custom command with the provided arguments.\\\",\n\t\t\\\"Command|The command to execute (path to a program etc.).\\\",\n\t\t\\\"Arguments|Launch arguments for the command.\\\",\n\t]\n\nfunc process_string(string: String) -> String:\n\tif string == \\\"$godot\\\":\n\t\treturn OS.get_executable_path()\n\telif string == \\\"$local_godot\\\":\n\t\treturn Data.get_godot_path()\n\t\n\treturn string.replace(\\\"$project\\\", Data.project_path)\n\"\n\n[node name=\"CustomTask\" type=\"GridContainer\"]\noffset_right = 506.0\noffset_bottom = 58.0\ncolumns = 2\nscript = SubResource(\"GDScript_3vtsj\")\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Command\"\nhorizontal_alignment = 2\n\n[node name=\"Command\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Arguments\"\nhorizontal_alignment = 2\n\n[node name=\"Arguments\" parent=\".\" instance=ExtResource(\"1_siigy\")]\nunique_name_in_owner = true\nlayout_mode = 2\n"
  },
  {
    "path": "Tasks/ExportProject.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://m4tjlhu18ode\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_ftqqe\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_mlhhy\"]\nscript/source = \"extends \\\"res://Tasks/ExportTask.gd\\\"\n\n@onready var preset_list: OptionButton = %PresetList\n@onready var debug: CheckBox = %Debug\n@onready var custom_path: Control = $CustomPath\n\nfunc _get_task_name() -> String:\n\treturn \\\"Export Project\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Export Project (%s)\\\" % get_preset_name()\n\nfunc _initialize():\n\tsuper()\n\tdefaults[\\\"preset\\\"] = \\\"\\\"\n\tdefaults[\\\"debug\\\"] = false\n\tdefaults[\\\"custom_path\\\"] = \\\"\\\"\n\t\n\tsetup_preset_list(preset_list)\n\nfunc _prepare():\n\tvar path: String\n\tif not custom_path.text.is_empty():\n\t\tpath = custom_path.text\n\t\toverride_path = true\n\telse:\n\t\tvar export_presets := load_presets()\n\t\tif export_presets:\n\t\t\tvar preset_name := get_preset_name()\n\t\t\tfor section in export_presets.get_sections():\n\t\t\t\tif export_presets.get_value(section, \\\"name\\\", \\\"\\\") == preset_name:\n\t\t\t\t\tpath = export_presets.get_value(section, \\\"export_path\\\", \\\"\\\")\n\t\t\t\t\tbreak\n\t\n\tset_export_path(path)\n\texport_debug = debug.button_pressed\n\texport_preset = get_preset_name()\n\t\n\tsuper()\n\nfunc _load():\n\tvar preset_text: String = data[\\\"preset\\\"]\n\tvar preset_assigned: bool\n\tfor i in preset_list.item_count:\n\t\tif preset_list.get_item_text(i) == preset_text:\n\t\t\tpreset_list.selected = i\n\t\t\tpreset_assigned = true\n\t\t\tbreak\n\t\n\tif not preset_assigned:\n\t\tpreset_list.selected = 0\n\t\tcustom_path.text = preset_text\n\t\n\tdebug.button_pressed = data[\\\"debug\\\"]\n\tcustom_path.text = data[\\\"custom_path\\\"]\n\nfunc _store():\n\tif preset_list.disabled:\n\t\tdata[\\\"preset\\\"] = \\\"\\\"\n\telse:\n\t\tdata[\\\"preset\\\"] = preset_list.get_item_text(preset_list.selected)\n\t\n\tdata[\\\"debug\\\"] = debug.button_pressed\n\tdata[\\\"custom_path\\\"] = custom_path.text\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Exports the project using one of defined export presets.\\\",\n\t\t\\\"Preset|Preset name from the project's presets defined in the Export dialog.\\\",\n\t\t\\\"Debug|If enabled, exports a debug build.\\\",\n\t]\n\nfunc get_preset_name() -> String:\n\treturn preset_list.get_item_text(preset_list.selected)\n\"\n\n[node name=\"ExportProject\" type=\"GridContainer\"]\noffset_right = 532.0\noffset_bottom = 120.0\ncolumns = 2\nscript = SubResource(\"GDScript_mlhhy\")\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Preset\"\nhorizontal_alignment = 2\n\n[node name=\"PresetList\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\nlayout_mode = 2\ntext = \"Custom Path\"\nhorizontal_alignment = 2\n\n[node name=\"CustomPath\" parent=\".\" instance=ExtResource(\"1_ftqqe\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\nmode = 2\nscope = 1\n\n[node name=\"Control\" type=\"Control\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\n\n[node name=\"Debug\" type=\"CheckBox\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 0\ntext = \"Debug\"\n"
  },
  {
    "path": "Tasks/ExportProjectFromTemplate.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://doi6iht30rdxc\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_mlhhy\"]\nscript/source = \"extends \\\"res://Tasks/ExportTask.gd\\\"\n\nconst CUSTOM_PRESET = \\\"_Project_Builds_\\\"\n\n@onready var preset_list: OptionButton = %PresetList\n@onready var template_list: OptionButton = %TemplateList\n@onready var path_suffix: LineEdit = %PathSuffix\n@onready var debug: CheckBox = %Debug\n\nvar custom_preset: int\n\nfunc _get_task_name() -> String:\n\treturn \\\"Export Project From Template\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Export Project From Template (preset %s, template %s)\\\" % [data[\\\"preset\\\"], data[\\\"template\\\"]]\n\nfunc _initialize():\n\tsuper()\n\tdefaults[\\\"preset\\\"] = \\\"\\\"\n\tdefaults[\\\"template\\\"] = \\\"\\\"\n\tdefaults[\\\"path_suffix\\\"] = \\\"\\\"\n\tdefaults[\\\"debug\\\"] = false\n\t\n\tsetup_preset_list(preset_list)\n\t\n\tfor template in Data.templates:\n\t\ttemplate_list.add_item(template[\\\"name\\\"])\n\t\n\tif template_list.item_count == 0:\n\t\ttemplate_list.add_item(\\\"No template found. Define in Preset Templates tab.\\\")\n\t\ttemplate_list.disabled = true\n\nfunc _prevalidate() -> bool:\n\tif not super():\n\t\treturn false\n\t\n\tvar template_name: String = template_list.get_item_text(template_list.selected)\n\tif Data.get_template(template_name).is_empty():\n\t\terror_message = \\\"Invalid export preset template: %s.\\\" % template_name\n\t\n\treturn error_message.is_empty()\n\nfunc _prepare() -> void:\n\tvar export_presets := load_presets()\n\tvar template := Data.get_template(template_list.get_item_text(template_list.selected))\n\t\n\tvar base_preset := preset_list.selected\n\tcustom_preset = preset_list.item_count\n\tfor section in export_presets.get_sections():\n\t\tif export_presets.get_value(section, \\\"name\\\", \\\"\\\") == CUSTOM_PRESET:\n\t\t\tcustom_preset = section.get_slice(\\\".\\\", 1).to_int()\n\t\t\tbreak\n\t\n\tvar new_section := \\\"preset.%d\\\" % custom_preset\n\tvar section := \\\"preset.%d\\\" % base_preset\n\tfor key in export_presets.get_section_keys(section):\n\t\texport_presets.set_value(new_section, key, export_presets.get_value(section, key))\n\t\n\texport_presets.set_value(new_section, \\\"name\\\", CUSTOM_PRESET)\n\texport_presets.set_value(new_section, \\\"custom_features\\\", \\\", \\\".join(template[\\\"custom_features\\\"]))\n\texport_presets.set_value(new_section, \\\"include_filter\\\", \\\", \\\".join(template[\\\"include_filters\\\"]))\n\texport_presets.set_value(new_section, \\\"exclude_filter\\\", \\\", \\\".join(template[\\\"exclude_filters\\\"]))\n\t\n\tif not template[\\\"export_path\\\"].is_empty():\n\t\tset_export_path(template[\\\"export_path\\\"].path_join(path_suffix.text))\n\t\texport_presets.set_value(new_section, \\\"export_path\\\", export_path)\n\telse:\n\t\tset_export_path(export_presets.get_value(new_section, \\\"export_path\\\", \\\"\\\"))\n\t\n\tnew_section = \\\"preset.%d.options\\\" % custom_preset\n\tsection = \\\"preset.%d.options\\\" % base_preset\n\tfor key in export_presets.get_section_keys(section):\n\t\texport_presets.set_value(new_section, key, export_presets.get_value(section, key))\n\t\n\texport_presets.save(preset_path)\n\t\n\texport_debug = debug.button_pressed\n\texport_preset = CUSTOM_PRESET\n\t\n\tsuper()\n\nfunc _cleanup() -> void:\n\tvar export_presets := load_presets()\n\tif not export_presets:\n\t\treturn\n\t\n\texport_presets.erase_section(\\\"preset.%d\\\" % custom_preset)\n\texport_presets.erase_section(\\\"preset.%d.options\\\" % custom_preset)\n\t\n\texport_presets.save(preset_path)\n\nfunc _load():\n\tvar text: String = data[\\\"preset\\\"]\n\tpreset_list.selected = 0\n\t\n\tfor i in preset_list.item_count:\n\t\tif preset_list.get_item_text(i) == text:\n\t\t\tpreset_list.selected = i\n\t\t\tbreak\n\t\n\ttext = data[\\\"template\\\"]\n\ttemplate_list.selected = 0\n\t\n\tfor i in template_list.item_count:\n\t\tif template_list.get_item_text(i) == text:\n\t\t\ttemplate_list.selected = i\n\t\t\tbreak\n\t\n\tpath_suffix.text = data[\\\"path_suffix\\\"]\n\tdebug.button_pressed = data[\\\"debug\\\"]\n\nfunc _store():\n\tif preset_list.disabled:\n\t\tdata[\\\"preset\\\"] = \\\"\\\"\n\telse:\n\t\tdata[\\\"preset\\\"] = preset_list.get_item_text(preset_list.selected)\n\t\n\tif template_list.disabled:\n\t\tdata[\\\"template\\\"] = \\\"\\\"\n\telse:\n\t\tdata[\\\"template\\\"] = template_list.get_item_text(template_list.selected)\n\t\n\tdata[\\\"path_suffix\\\"] = path_suffix.text\n\tdata[\\\"debug\\\"] = debug.button_pressed\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Exports the project by creating a new preset using the selected template. An existing preset is used as a base.\\\",\n\t\t\\\"Base Preset|Preset name from the project's presets defined in the Export dialog.\\\",\n\t\t\\\"Preset Template|Template name from the templates defined in Project Builder.\\\",\n\t\t\\\"Debug|If enabled, exports a debug build.\\\",\n\t]\n\"\n\n[node name=\"ExportProjectFromTemplate\" type=\"GridContainer\"]\noffset_right = 532.0\noffset_bottom = 120.0\ncolumns = 2\nscript = SubResource(\"GDScript_mlhhy\")\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Base Preset\"\nhorizontal_alignment = 2\n\n[node name=\"PresetList\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nlayout_mode = 2\ntext = \"Preset Template\"\nhorizontal_alignment = 2\n\n[node name=\"TemplateList\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label3\" type=\"Label\" parent=\".\"]\nlayout_mode = 2\ntext = \"Path Suffix\"\nhorizontal_alignment = 2\n\n[node name=\"PathSuffix\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Control\" type=\"Control\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"Debug\" type=\"CheckBox\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 0\ntext = \"Debug\"\n"
  },
  {
    "path": "Tasks/ExportTask.gd",
    "content": "extends Task\n\nstatic var godot_path: String\nstatic var is_godot_3: bool\n\nvar export_debug: bool\nvar override_path: bool\nvar preset_path: String\nvar export_path: String\nvar export_preset: String\n\nfunc _init() -> void:\n\thas_static_configuration = true\n\nstatic func _initialize_project():\n\tupdate_godot_version()\n\nfunc _initialize():\n\tpreset_path = Data.project_path.path_join(\"export_presets.cfg\")\n\t\n\tif godot_path != Data.get_godot_path():\n\t\tupdate_godot_version()\n\nfunc _prevalidate() -> bool:\n\tif OS.execute(godot_path, [\"--version\"], []) != OK:\n\t\terror_message = \"Godot executable (%s) is not valid.\" % godot_path\n\t\treturn false\n\t\n\tvar project := ConfigFile.new()\n\tproject.load(Data.project_path.path_join(\"project.godot\"))\n\tif project.get_value(\"\", \"config_version\", 0) == 4 and not is_godot_3:\n\t\terror_message = \"Trying to export Godot 3 project using Godot 4 executable.\"\n\t\treturn false\n\t\n\tvar presets := load_presets()\n\tif not presets:\n\t\terror_message = \"Export presets file does not exist.\"\n\t\treturn false\n\t\n\treturn true\n\nfunc _get_command() -> String:\n\treturn Data.get_godot_path()\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret: PackedStringArray\n\t\n\tif is_godot_3:\n\t\tret.append(\"--no-window\")\n\telse:\n\t\tret.append(\"--headless\")\n\t\n\tret.append(\"--path\")\n\tret.append(Data.project_path)\n\t\n\tif export_debug:\n\t\tret.append(\"--export-debug\")\n\telif is_godot_3:\n\t\tret.append(\"--export\")\n\telse:\n\t\tret.append(\"--export-release\")\n\t\n\tret.append(export_preset)\n\t\n\tif override_path:\n\t\tret.append(export_path)\n\t\n\treturn ret\n\nfunc _prepare():\n\tvar base_dir := export_path.get_base_dir()\n\tDirAccess.make_dir_recursive_absolute(base_dir)\n\nstatic func update_godot_version():\n\tgodot_path = Data.get_godot_path()\n\tvar output: Array\n\tif OS.execute(godot_path, [\"--version\"], output) == OK:\n\t\tif output[0].begins_with(\"4\"):\n\t\t\tis_godot_3 = false\n\t\telse:\n\t\t\tis_godot_3 = true\n\nfunc load_presets() -> ConfigFile:\n\tvar config_file := ConfigFile.new()\n\tif config_file.load(preset_path) == OK:\n\t\treturn config_file\n\treturn null\n\nfunc set_export_path(path: String):\n\tif path.begins_with(\"res://\"):\n\t\texport_path = path.replace(\"res:/\", Data.project_path)\n\telse:\n\t\texport_path = Data.project_path.path_join(path)\n\nfunc setup_preset_list(list: OptionButton):\n\tvar export_presets := load_presets()\n\t\n\tif export_presets:\n\t\tfor section in export_presets.get_sections():\n\t\t\tif section.ends_with(\"options\"):\n\t\t\t\tcontinue\n\t\t\t\n\t\t\tlist.add_item(export_presets.get_value(section, \"name\"))\n\t\n\tif list.item_count == 0:\n\t\tlist.add_item(\"No presets found in export_presets.cfg.\")\n\t\tlist.disabled = true\n"
  },
  {
    "path": "Tasks/ExportTask.gd.uid",
    "content": "uid://ddyec862cgxyf\n"
  },
  {
    "path": "Tasks/PackZIP.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://dw4t3o5hj774w\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_smuq5\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_ess8h\"]\nscript/source = \"extends \\\"ScriptTask.gd\\\"\n\n@onready var source_directory: Control = %SourceDirectory\n@onready var target_path: Control = %TargetPath\n@onready var file_filter: LineEdit = %FileFilter\n@onready var file_filter2: LineEdit = %FileFilter2\n\nfunc _get_task_name() -> String:\n\treturn \\\"Pack ZIP\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Pack folder \\\\\\\"%s\\\\\\\" into \\\\\\\"%s\\\\\\\"\\\" % [source_directory.text.get_file(), target_path.text.get_file()]\n\nfunc _initialize() -> void:\n\tdefaults[\\\"source\\\"] = \\\"\\\"\n\tdefaults[\\\"destination\\\"] = \\\"\\\"\n\tdefaults[\\\"include_files\\\"] = \\\"\\\"\n\tdefaults[\\\"exclude_files\\\"] = \\\"\\\"\n\nfunc _validate() -> bool:\n\tvar path := Data.resolve_path(source_directory.text)\n\tif not DirAccess.dir_exists_absolute(path):\n\t\terror_message = \\\"Source directory (%s) does not exist.\\\" % path\n\t\treturn false\n\t\n\treturn true\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret := super()\n\t\n\tret.append(Data.resolve_path(source_directory.text))\n\tret.append(Data.resolve_path(target_path.text))\n\t\n\tif not file_filter.text.is_empty():\n\t\tret.append(\\\"--include\\\")\n\t\tret.append_array(file_filter.text.split(\\\" \\\"))\n\tif not file_filter2.text.is_empty():\n\t\tret.append(\\\"--exclude\\\")\n\t\tret.append_array(file_filter2.text.split(\\\" \\\"))\n\t\n\treturn ret\n\nfunc _load():\n\tsource_directory.text = data[\\\"source\\\"]\n\ttarget_path.text = data[\\\"destination\\\"]\n\tfile_filter.text = data[\\\"include_files\\\"]\n\tfile_filter2.text = data[\\\"exclude_files\\\"]\n\nfunc _store():\n\tdata[\\\"source\\\"] = source_directory.text\n\tdata[\\\"destination\\\"] = target_path.text\n\tdata[\\\"include_files\\\"] = file_filter.text\n\tdata[\\\"exclude_files\\\"] = file_filter2.text\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Packs specified files in a ZIP archive.\\\",\n\t\t\\\"Source Directory|Files from this directory will be packed.\\\",\n\t\t\\\"Target File Path|Path of the target ZIP archive.\\\",\n\t\t\\\"Include Files|List of filters for files from the directory. Example: \\\\\\\"*.exe *.pck\\\\\\\". If empty, all files will be packed. Otherwise only files that match the filter will be included.\\\",\n\t\t\\\"Exclude Files|Filters files from the directory. Files that match this filter will be excluded.\\\",\n\t]\n\"\n\n[node name=\"MakeZip\" type=\"VBoxContainer\"]\noffset_right = 734.0\noffset_bottom = 101.0\nscript = SubResource(\"GDScript_ess8h\")\nscript_name = \"PackZIP.gd\"\n\n[node name=\"GridContainer\" type=\"GridContainer\" parent=\".\"]\nlayout_mode = 2\ncolumns = 2\n\n[node name=\"Label2\" type=\"Label\" parent=\"GridContainer\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Source Directory\"\nhorizontal_alignment = 2\n\n[node name=\"SourceDirectory\" parent=\"GridContainer\" instance=ExtResource(\"1_smuq5\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nscope = 1\nmissing_mode = 1\n\n[node name=\"Label3\" type=\"Label\" parent=\"GridContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Target File Path\"\nhorizontal_alignment = 2\n\n[node name=\"TargetPath\" parent=\"GridContainer\" instance=ExtResource(\"1_smuq5\")]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\nmode = 2\nscope = 1\nfilters = PackedStringArray(\"*.zip\")\n\n[node name=\"Label\" type=\"Label\" parent=\"GridContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Include Files\"\nhorizontal_alignment = 2\n\n[node name=\"FileFilter\" type=\"LineEdit\" parent=\"GridContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nplaceholder_text = \"Space-separated list of filters, e.g. \\\"*.exe\\\". Leave empty to include all.\"\n\n[node name=\"Label4\" type=\"Label\" parent=\"GridContainer\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Exclude Files\"\nhorizontal_alignment = 2\n\n[node name=\"FileFilter2\" type=\"LineEdit\" parent=\"GridContainer\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\nplaceholder_text = \"Space-separated list of filters. Leave empty to exclude none.\"\n"
  },
  {
    "path": "Tasks/ScriptTask/BaseScriptTask.gd",
    "content": "extends SceneTree\n\nvar argument_list: PackedStringArray\nvar arguments: Dictionary\n\nfunc add_expected_argument(name: String, description: String):\n\targument_list.append(\"e|%s|%s\" % [name, description])\n\nfunc add_optional_argument(name: String, description: String):\n\targument_list.append(\"o|%s|%s\" % [name, description])\n\nfunc add_variadic_argument(name: String, description: String):\n\targument_list.append(\"v|%s|%s\" % [name, description])\n\nfunc fetch_arguments() -> bool:\n\tvar args := OS.get_cmdline_user_args()\n\tif not args.is_empty() and args[0] == \"--help\":\n\t\tprint(\"Argument list:\")\n\t\tfor argument in argument_list:\n\t\t\tvar type := argument.get_slice(\"|\", 0)\n\t\t\t\n\t\t\tprint(\"%s (%s) - %s\" % [\n\t\t\t\targument.get_slice(\"|\", 1),\n\t\t\t\t\"expected\" if type == \"e\" else \"optional\" if type == \"o\" else \"variadic\",\n\t\t\t\targument.get_slice(\"|\", 2),\n\t\t\t])\n\t\t\n\t\tquit(OK)\n\t\treturn false\n\t\n\tfor i in argument_list.size():\n\t\tvar argument := argument_list[i]\n\t\tvar argument_name := argument.get_slice(\"|\", 1)\n\t\t\n\t\tif argument.begins_with(\"e\") and i >= args.size():\n\t\t\tprinterr(\"Missing expected argument: %s\" % argument_name)\n\t\t\tquit(ERR_INVALID_PARAMETER)\n\t\t\treturn false\n\t\t\n\t\tif i >= args.size():\n\t\t\tif argument.begins_with(\"v\"):\n\t\t\t\targuments[argument_name] = []\n\t\t\telse:\n\t\t\t\targuments[argument_name] = \"\"\n\t\t\tcontinue\n\t\t\n\t\tif argument.begins_with(\"v\"):\n\t\t\targuments[argument_name] = args.slice(i)\n\t\telse:\n\t\t\targuments[argument_name] = args[i]\n\t\n\treturn true\n"
  },
  {
    "path": "Tasks/ScriptTask/BaseScriptTask.gd.uid",
    "content": "uid://dj2qru8dp0yi2\n"
  },
  {
    "path": "Tasks/ScriptTask/ClearDirectory.gd",
    "content": "extends \"BaseScriptTask.gd\"\n\nfunc _init() -> void:\n\tadd_expected_argument(\"source\", \"Directory to clear.\")\n\tadd_variadic_argument(\"filters\", \"List of filters.\")\n\t\n\tif not fetch_arguments():\n\t\treturn\n\t\n\tvar directory_path: String = arguments[\"source\"]\n\tvar include_filters: PackedStringArray\n\tvar exclude_filters: PackedStringArray\n\t\n\tvar mode := 0\n\tfor arg: String in arguments[\"filters\"]:\n\t\tif arg == \"--include\":\n\t\t\tmode = 1\n\t\telif arg == \"--exclude\":\n\t\t\tmode = 2\n\t\telse:\n\t\t\tif mode == 1:\n\t\t\t\tinclude_filters.append(arg)\n\t\t\telif mode == 2:\n\t\t\t\texclude_filters.append(arg)\n\t\n\tDirAccess.make_dir_recursive_absolute(\"user://Trash\")\n\t\n\tvar counter: int\n\tvar fail_counter: int\n\tfor file in DirAccess.get_files_at(directory_path):\n\t\tvar skip := not include_filters.is_empty()\n\t\tfor filter in include_filters:\n\t\t\tif file.match(filter):\n\t\t\t\tskip = false\n\t\t\t\tbreak\n\t\t\n\t\tif not skip:\n\t\t\tfor filter in exclude_filters:\n\t\t\t\tif file.match(filter):\n\t\t\t\t\tskip = true\n\t\t\t\t\tbreak\n\t\t\n\t\tif skip:\n\t\t\tcontinue\n\t\t\n\t\tprint(\"Removing file: %s\" % file)\n\t\tvar error := DirAccess.rename_absolute(directory_path.path_join(file), \"user://Trash\".path_join(file))\n\t\tif error == OK:\n\t\t\tcounter += 1\n\t\telse:\n\t\t\tfail_counter += 1\n\t\t\tprinterr(\"Failed! Error: %d\" % error)\n\t\n\tif fail_counter > 0:\n\t\tprint(\"Cleanup finished. Cleared %d files, %d files failed.\" % [counter, fail_counter])\n\telif counter > 0:\n\t\tprint(\"Cleanup finished successfully. Cleared %d files.\" % counter)\n\telse:\n\t\tprint(\"No files to cleanup.\")\n\t\n\tquit(OK)\n"
  },
  {
    "path": "Tasks/ScriptTask/ClearDirectory.gd.uid",
    "content": "uid://br3t3y5s843pj\n"
  },
  {
    "path": "Tasks/ScriptTask/CopyFiles.gd",
    "content": "extends \"BaseScriptTask.gd\"\n\nfunc _init() -> void:\n\tadd_expected_argument(\"source\", \"Source directory for files.\")\n\tadd_expected_argument(\"destination\", \"Destination directory for files.\")\n\tadd_expected_argument(\"mode\", \"Either \\\"file\\\", \\\"dir\\\" or \\\"dir_recursive\\\".\")\n\t\n\tif not fetch_arguments():\n\t\treturn\n\t\n\tvar source_path: String = arguments[\"source\"]\n\tvar target_path: String = arguments[\"destination\"]\n\tvar folder_mode: bool = arguments[\"mode\"] != \"file\"\n\tvar recursive: bool = arguments[\"mode\"].ends_with(\"recursive\")\n\t\n\tvar error: int = OK\n\tif folder_mode:\n\t\terror = copy_folder(source_path, target_path, recursive)\n\telse:\n\t\tif target_path.ends_with(\"/\"):\n\t\t\terror = copy_file(source_path, target_path.path_join(source_path.get_file()))\n\t\telse:\n\t\t\terror = copy_file(source_path, target_path)\n\t\n\tif error == OK:\n\t\tprint(\"Copying finished successfully.\")\n\telse:\n\t\tprinterr(\"Copying failed, check error code.\")\n\t\n\tquit(error)\n\nfunc copy_folder(source_path: String, target_path: String, recursive: bool) -> int:\n\tfor file in DirAccess.get_files_at(source_path):\n\t\tvar error := copy_file(source_path.path_join(file), target_path.path_join(file))\n\t\tif error != OK:\n\t\t\treturn error\n\t\n\tif recursive:\n\t\tfor dir in DirAccess.get_directories_at(source_path):\n\t\t\tvar error := copy_folder(source_path.path_join(dir), target_path.path_join(dir), true)\n\t\t\tif error != OK:\n\t\t\t\treturn error\n\t\n\treturn OK\n\nfunc copy_file(from: String, to: String) -> int:\n\tif from == to:\n\t\tprinterr(\"Source and target file path are the same.\")\n\t\treturn ERR_INVALID_PARAMETER\n\t\n\tvar error := DirAccess.make_dir_recursive_absolute(to.get_base_dir())\n\tif error != OK:\n\t\treturn error\n\t\n\tprint(\"Copying %s to %s\" % [from, to])\n\treturn DirAccess.copy_absolute(from, to)\n"
  },
  {
    "path": "Tasks/ScriptTask/CopyFiles.gd.uid",
    "content": "uid://dkrhktoogubcl\n"
  },
  {
    "path": "Tasks/ScriptTask/PackZIP.gd",
    "content": "extends \"BaseScriptTask.gd\"\n\nvar root_path: String\nvar include_filters: PackedStringArray\nvar exclude_filters: PackedStringArray\n\nvar quit_error: int\n\nfunc _init() -> void:\n\tadd_expected_argument(\"source\", \"Source folder with files.\")\n\tadd_expected_argument(\"destination\", \"Destination file path.\")\n\tadd_variadic_argument(\"filters\", \"List of filters.\")\n\t\n\tif not fetch_arguments():\n\t\treturn\n\t\n\troot_path = arguments[\"source\"]\n\tvar target_path: String = arguments[\"destination\"]\n\t\n\tvar mode := 0\n\tfor arg: String in arguments[\"filters\"]:\n\t\tif arg == \"--include\":\n\t\t\tmode = 1\n\t\telif arg == \"--exclude\":\n\t\t\tmode = 2\n\t\telse:\n\t\t\tif mode == 1:\n\t\t\t\tinclude_filters.append(arg)\n\t\t\telif mode == 2:\n\t\t\t\texclude_filters.append(arg)\n\t\n\tvar zip := ZIPPacker.new()\n\tprint(\"Creating ZIP file: %s\" % target_path)\n\t\n\tvar error := zip.open(target_path)\n\tif error != OK:\n\t\tprinterr(\"Creating failed, error %d\" % error)\n\t\tquit(error)\n\t\treturn\n\t\n\tpack_files(zip, root_path)\n\t\n\tzip.close()\n\tif quit_error == OK:\n\t\tprint(\"Packing finished successfully!\")\n\telse:\n\t\tprinterr(\"Packing failed, check error code.\")\n\t\n\tquit(quit_error)\n\nfunc pack_files(zip: ZIPPacker, dir: String):\n\tvar da := DirAccess.open(dir)\n\tif not da:\n\t\tprinterr(\"Error opening directory: %s\" % dir)\n\t\tquit_error = DirAccess.get_open_error()\n\t\treturn\n\t\n\tda.include_hidden = true\n\t\n\tfor file in da.get_files():\n\t\tvar skip := not include_filters.is_empty()\n\t\tfor filter in include_filters:\n\t\t\tif file.match(filter):\n\t\t\t\tskip = false\n\t\t\t\tbreak\n\t\t\n\t\tif not skip:\n\t\t\tfor filter in exclude_filters:\n\t\t\t\tif file.match(filter):\n\t\t\t\t\tskip = true\n\t\t\t\t\tbreak\n\t\t\n\t\tif not skip:\n\t\t\tpack(zip, dir.path_join(file))\n\t\t\n\t\tif quit_error != OK:\n\t\t\treturn\n\t\n\tfor d in da.get_directories():\n\t\tpack_files(zip, dir.path_join(d))\n\t\t\n\t\tif quit_error != OK:\n\t\t\treturn\n\nfunc pack(zip: ZIPPacker, file: String):\n\tvar target_file := file.trim_prefix(root_path + \"/\")\n\t\n\tprint(\"Packing file: %s\" % target_file)\n\tvar data := FileAccess.get_file_as_bytes(file)\n\tif data.is_empty():\n\t\tvar error := FileAccess.get_open_error()\n\t\tif error != OK:\n\t\t\tprinterr(\"Error reading file: %d\" % error)\n\t\t\tquit_error = error\n\t\t\treturn\n\t\n\tzip.start_file(target_file)\n\tzip.write_file(data)\n\tzip.close_file()\n"
  },
  {
    "path": "Tasks/ScriptTask/PackZIP.gd.uid",
    "content": "uid://ry5v70sx1vat\n"
  },
  {
    "path": "Tasks/ScriptTask.gd",
    "content": "extends Task\n\n@export var script_name: String\n\nfunc _get_command() -> String:\n\treturn OS.get_executable_path()\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret: PackedStringArray\n\tret.append(\"--headless\")\n\t\n\tif OS.has_feature(\"editor\"):\n\t\tret.append(\"--path\")\n\t\tret.append(Data.get_res_path())\n\t\n\tret.append(\"--script\")\n\tret.append(get_script_path())\n\t\n\tret.append(\"--\")\n\treturn ret\n\nfunc _prevalidate() -> bool:\n\tif script_name.is_empty():\n\t\terror_message = \"ScriptTask's script name is empty. Assign it in the scene.\"\n\t\treturn false\n\t\n\tvar script_path := get_script_path()\n\tif not ResourceLoader.exists(script_path):\n\t\terror_message = \"The provided ScriptTask's script does not exist at expected path: \\\"%s\\\".\" % script_path\n\t\treturn false\n\t\n\treturn true\n\nfunc get_script_path() -> String:\n\tvar scr: Script = get_script()\n\twhile not scr.resource_path.get_file() == \"ScriptTask.gd\":\n\t\tscr = scr.get_base_script()\n\t\n\treturn scr.resource_path.get_base_dir().path_join(\"ScriptTask/%s\" % script_name)\n"
  },
  {
    "path": "Tasks/ScriptTask.gd.uid",
    "content": "uid://cu6cbkw8roupo\n"
  },
  {
    "path": "Tasks/SubRoutine.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://dh4e5n3xxf3n\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_boi7e\"]\nscript/source = \"extends Task\n\n@onready var limbo: Node2D = %Limbo\n@onready var sub_routine_list: OptionButton = %SubRoutine\n\nvar task_instances: Array[Task]\nvar task_index: int\nvar current_task: Task\n\nfunc _get_task_name() -> String:\n\treturn \\\"Sub-Routine\\\"\n\nfunc _get_execute_string() -> String:\n\tif current_task:\n\t\treturn \\\"Sub-Routine: %s\\\" % current_task._get_task_name()\n\telse:\n\t\treturn _get_task_name()\n\nfunc _initialize():\n\tif not is_node_ready():\n\t\tawait ready\n\t\n\tfor i in Data.routines.size():\n\t\tvar rout := Data.routines[i]\n\t\tif rout == Data.current_routine:\n\t\t\tcontinue\n\t\t\n\t\tsub_routine_list.add_item(rout[\\\"name\\\"])\n\t\tsub_routine_list.set_item_metadata(-1, rout[\\\"name\\\"])\n\t\n\tif sub_routine_list.item_count == 0:\n\t\tsub_routine_list.add_item(\\\"No other routine found. Create new routine.\\\")\n\t\tsub_routine_list.set_item_metadata(-1, \\\"\\\")\n\t\tsub_routine_list.disabled = true\n\nfunc _prevalidate() -> bool:\n\tif not task_instances.is_empty():\n\t\treturn true\n\t\n\tvar routine_name: String = data[\\\"routine\\\"]\n\t\n\tvar task_list: Array[Dictionary]\n\tfor rout in Data.routines:\n\t\tif rout[\\\"name\\\"] == routine_name:\n\t\t\ttask_list = rout[\\\"tasks\\\"]\n\t\t\tbreak\n\t\n\tif task_list.is_empty():\n\t\terror_message = \\\"Invalid routine name or no tasks.\\\"\n\t\treturn false\n\t\n\tvar current_routine_name: String = Data.current_routine[\\\"name\\\"]\n\tvar i := 0\n\twhile i < task_list.size():\n\t\tvar task := task_list[i]\n\t\tif task[\\\"scene\\\"] == \\\"SubRoutine\\\":\n\t\t\tvar subroutine_name: String = task[\\\"data\\\"][\\\"routine\\\"]\n\t\t\tif detect_loop([current_routine_name], subroutine_name):\n\t\t\t\terror_message = \\\"Cyclic Sub-Routine detected.\\\"\n\t\t\t\treturn false\n\t\t\t\n\t\t\tvar subtask_list: Array[Dictionary]\n\t\t\tfor rout in Data.routines:\n\t\t\t\tif rout[\\\"name\\\"] == subroutine_name:\n\t\t\t\t\tsubtask_list = rout[\\\"tasks\\\"]\n\t\t\t\t\tbreak\n\t\t\t\n\t\t\ttask_list = task_list.slice(0, i) + subtask_list + task_list.slice(i + 1)\n\t\t\tcontinue\n\t\t\n\t\tvar task_instance := Task.create_instance(task[\\\"scene\\\"])\n\t\tlimbo.add_child(task_instance)\n\t\ttask_instance._initialize()\n\t\ttask_instance.load_data(task[\\\"data\\\"])\n\t\ttask_instances.append(task_instance)\n\t\t\n\t\tvar result := task_instance._prevalidate()\n\t\tif not result:\n\t\t\tcurrent_task = task_instance\n\t\t\terror_message = task_instance.error_message\n\t\t\treturn false\n\t\t\n\t\ti += 1\n\t\n\tcurrent_task = task_instances[0]\n\treturn true\n\nfunc _validate() -> bool:\n\tcurrent_task = task_instances[task_index]\n\t\n\tvar result := current_task._validate()\n\terror_message = current_task.error_message\n\treturn result\n\nfunc _get_command() -> String:\n\treturn current_task._get_command()\n\nfunc _get_arguments() -> PackedStringArray:\n\treturn current_task._get_arguments()\n\nfunc _prepare() -> void:\n\tcurrent_task._prepare()\n\nfunc _cleanup() -> void:\n\tcurrent_task._cleanup()\n\ttask_index += 1\n\t\n\tif task_index < task_instances.size():\n\t\tvar next: Task = load(scene_file_path).instantiate()\n\t\tnext.task_instances = task_instances\n\t\tnext.task_index = task_index\n\t\t\n\t\tget_parent().add_child(next)\n\t\tget_parent().move_child(next, get_index() + 1)\n\nfunc _load() -> void:\n\tvar routine_name: String = data[\\\"routine\\\"]\n\t\n\tvar found: bool\n\tfor i in sub_routine_list.item_count:\n\t\tif sub_routine_list.get_item_metadata(i) == routine_name:\n\t\t\tsub_routine_list.selected = i\n\t\t\tfound = true\n\t\t\tbreak\n\t\n\tif not found:\n\t\tsub_routine_list.selected = -1\n\nfunc _store() -> void:\n\tdata[\\\"routine\\\"] = sub_routine_list.get_selected_metadata()\n\tif not data[\\\"routine\\\"]:\n\t\tdata[\\\"routine\\\"] = \\\"\\\"\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Inlines another routine to allow running other routines as part of routines.\\\",\n\t\t\\\"Routine|Name of the routine to run. Automatically lists all configured routines.\\\"\n\t]\n\nfunc detect_loop(base: PackedStringArray, routine: String) -> bool:\n\tbase.append(routine)\n\tfor rout in Data.routines:\n\t\tif rout[\\\"name\\\"] == routine:\n\t\t\tfor task in rout[\\\"tasks\\\"]:\n\t\t\t\tif task[\\\"scene\\\"] == \\\"SubRoutine\\\":\n\t\t\t\t\tvar routine2: String = task[\\\"data\\\"][\\\"routine\\\"]\n\t\t\t\t\tif routine2 in base:\n\t\t\t\t\t\treturn true\n\t\t\t\t\t\n\t\t\t\t\tif detect_loop(base, routine2):\n\t\t\t\t\t\treturn true\n\t\t\tbreak\n\treturn false\n\"\n\n[node name=\"SubRoutine\" type=\"GridContainer\"]\noffset_right = 377.0\noffset_bottom = 23.0\ncolumns = 2\nscript = SubResource(\"GDScript_boi7e\")\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Routine\"\nhorizontal_alignment = 2\n\n[node name=\"SubRoutine\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Limbo\" type=\"Node2D\" parent=\".\"]\nunique_name_in_owner = true\nvisible = false\nposition = Vector2(0, 27)\n"
  },
  {
    "path": "Tasks/UploadEpic.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://228qq6rkrwx6\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_nh5oq\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_qbsc0\"]\nscript/source = \"extends Task\n\n@onready var build_root: HBoxContainer = $BuildRoot\n@onready var executable_name: LineEdit = $ExecutableName\n@onready var version_prefix: LineEdit = $VersionPrefix\n\nvar project_version: String\n\nfunc _get_task_name() -> String:\n\treturn \\\"Upload Epic\\\"\n\nfunc _initialize() -> void:\n\tdefaults[\\\"build_root\\\"] = \\\"\\\"\n\tdefaults[\\\"executable_name\\\"] = \\\"\\\"\n\tdefaults[\\\"version_prefix\\\"] = \\\"\\\"\n\tproject_version = Data.get_project_version()\n\nfunc _prevalidate() -> bool:\n\tif not Data.global_config[\\\"epic_client_secret_env_var\\\"].is_empty():\n\t\tvar env := OS.get_environment(Data.global_config[\\\"epic_client_secret_env_var\\\"])\n\t\tif env.is_empty():\n\t\t\terror_message = \\\"The provided Epic client secret env variable does not exist.\\\"\n\t\t\treturn false\n\telif Data.global_config[\\\"epic_client_secret\\\"].is_empty():\n\t\terror_message = \\\"Epic client secret is empty and no env variable was provided.\\\"\n\t\treturn false\n\t\n\tif Data.global_config[\\\"build_patch_tool_path\\\"].is_empty():\n\t\terror_message = \\\"Build Patch Tool path is empty.\\\"\n\telif not FileAccess.file_exists(Data.global_config[\\\"build_patch_tool_path\\\"]):\n\t\terror_message = \\\"Build Patch Tool path does not point to any file.\\\"\n\telif Data.global_config[\\\"epic_organization_id\\\"].is_empty():\n\t\terror_message = \\\"Epic organization ID is empty.\\\"\n\telif Data.global_config[\\\"epic_client_id\\\"].is_empty():\n\t\terror_message = \\\"Epic client ID is empty.\\\"\n\telif Data.local_config[\\\"epic_product_id\\\"].is_empty():\n\t\terror_message = \\\"Epic product ID is empty.\\\"\n\telif Data.local_config[\\\"epic_artifact_id\\\"].is_empty():\n\t\terror_message = \\\"Epic artifact ID is empty.\\\"\n\telif executable_name.text.is_empty():\n\t\terror_message = \\\"Executable name is empty.\\\"\n\telif project_version.length() < 1 or project_version.length() > 100:\n\t\terror_message = \\\"Version string invalid (application/config/version). Length must be between 1 and 100 (inclusive).\\\"\n\telse:\n\t\tvar reg := RegEx.create_from_string(r\\\"^[a-zA-Z0-9\\\\.\\\\+-_]*$\\\")\n\t\tif not reg.search(project_version):\n\t\t\terror_message = \\\"Version string invalid (application/config/version). Use only these characters: a-z, A-Z, 0-9, or .+-_\\\"\n\t\n\tif not Data.global_config[\\\"epic_client_secret_env_var\\\"].is_empty():\n\t\tvar env := OS.get_environment(Data.global_config[\\\"epic_client_secret_env_var\\\"])\n\t\tif env.is_empty():\n\t\t\terror_message = \\\"The provided Epic client secret env variable does not exist.\\\"\n\telif Data.global_config[\\\"epic_client_secret\\\"].is_empty():\n\t\terror_message = \\\"Epic client secret is empty and no env variable was provided.\\\"\n\t\n\treturn error_message.is_empty()\n\nfunc _validate() -> bool:\n\tif not DirAccess.dir_exists_absolute(Data.project_path.path_join(build_root.text)):\n\t\terror_message = \\\"The provided build root folder does not exist.\\\"\n\t\treturn false\n\telif not FileAccess.file_exists(Data.project_path.path_join(build_root.text).path_join(executable_name.text)):\n\t\terror_message = \\\"The executable does not exist in build root folder.\\\"\n\t\treturn false\n\treturn true\n\nfunc _get_command() -> String:\n\treturn Data.global_config[\\\"build_patch_tool_path\\\"]\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar env_var: String = Data.global_config[\\\"epic_client_secret_env_var\\\"]\n\t\n\tvar ret: PackedStringArray\n\tret.append(\\\"-mode=UploadBinary\\\")\n\tret.append(\\\"-OrganizationId=\\\\\\\"%s\\\\\\\"\\\" % Data.global_config[\\\"epic_organization_id\\\"])\n\tret.append(\\\"-ClientId=\\\\\\\"%s\\\\\\\"\\\" % Data.global_config[\\\"epic_client_id\\\"])\n\tif env_var.is_empty():\n\t\tret.append(\\\"-ClientSecret=\\\\\\\"%s\\\\\\\"\\\" % Data.global_config[\\\"epic_client_secret\\\"])\n\telse:\n\t\tret.append(\\\"-ClientSecretEnvVar=\\\\\\\"%s\\\\\\\"\\\" % env_var)\n\tret.append(\\\"-ProductId=\\\\\\\"%s\\\\\\\"\\\" % Data.local_config[\\\"epic_product_id\\\"])\n\tret.append(\\\"-ArtifactId=\\\\\\\"%s\\\\\\\"\\\" % Data.local_config[\\\"epic_artifact_id\\\"])\n\tret.append(\\\"-BuildRoot=\\\\\\\"%s\\\\\\\"\\\" % build_root.text)\n\tret.append(\\\"-BuildVersion=\\\\\\\"%s-%s\\\\\\\"\\\" % [version_prefix.text, Data.get_project_version()])\n\tret.append(\\\"-AppLaunch=\\\\\\\"%s\\\\\\\"\\\" % executable_name.text)\n\t#ret.append(\\\"-AppArgs=\\\\\\\"\\\\\\\"\\\")\n\tret.append(\\\"-CloudDir=\\\\\\\"%s\\\\\\\"\\\" % Data.project_path.path_join(Data.local_config[\\\"epic_cloud_dir\\\"]))\n\t\n\treturn ret\n\nfunc _load():\n\tbuild_root.text = data[\\\"build_root\\\"]\n\texecutable_name.text = data[\\\"executable_name\\\"]\n\tversion_prefix.text = data[\\\"version_prefix\\\"]\n\nfunc _store():\n\tdata[\\\"build_root\\\"] = build_root.text\n\tdata[\\\"executable_name\\\"] = executable_name.text\n\tdata[\\\"version_prefix\\\"] = version_prefix.text\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Uploads files to Epic Games using Build Patch Tool.\\\",\n\t\t\\\"Build Root|The folder with your exported game content.\\\",\n\t\t\\\"Executable Name|Name of the executable used when launching the game.\\\",\n\t\t\\\"Version Prefix|Each upload needs unique version string, so use this option to add a (platform-specific etc.) prefix to your base version.\\\",\n\t]\n\"\n\n[node name=\"UploadEpic\" type=\"GridContainer\"]\noffset_right = 422.0\noffset_bottom = 101.0\ncolumns = 2\nscript = SubResource(\"GDScript_qbsc0\")\nhas_sensitive_data = true\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Build Root\"\nhorizontal_alignment = 2\n\n[node name=\"BuildRoot\" parent=\".\" instance=ExtResource(\"1_nh5oq\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\nscope = 1\nmissing_mode = 1\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Executable Name\"\nhorizontal_alignment = 2\n\n[node name=\"ExecutableName\" type=\"LineEdit\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label3\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Version Prefix\"\nhorizontal_alignment = 2\n\n[node name=\"VersionPrefix\" type=\"LineEdit\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\n"
  },
  {
    "path": "Tasks/UploadGOG.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://7qw6mxp834ei\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_a0t4t\"]\nscript/source = \"extends Task\n\n@onready var json_file: OptionButton = %JSONFile\n@onready var branch: LineEdit = %Branch\n@onready var branch_password: LineEdit = %BranchPassword\n\nstatic var json_list: PackedStringArray\n\nfunc _get_task_name() -> String:\n\treturn \\\"Upload GOG\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"GOG Build (%s)\\\" % json_file.text\n\nstatic func _initialize_project():\n\tvar cache_file := get_cache_file(\\\"GOGJSON\\\", FileAccess.READ)\n\tif cache_file:\n\t\tjson_list = str_to_var(cache_file.get_as_text())\n\telse:\n\t\tjson_list.clear()\n\nstatic func _begin_project_scan() -> void:\n\tjson_list.clear()\n\nstatic func _process_file(file: String):\n\tif file.get_extension() != \\\"json\\\":\n\t\treturn\n\t\n\tvar json: JSON = load(file)\n\tif not json.data is Dictionary:\n\t\treturn\n\t\n\tvar json_data: Dictionary = json.data\n\tif not \\\"project\\\" in json_data or not \\\"baseProductId\\\" in json_data[\\\"project\\\"]:\n\t\treturn\n\t\n\tjson_list.append(file.trim_prefix(Data.project_path + \\\"/\\\"))\n\nstatic func _end_project_scan() -> void:\n\tvar cache_file := get_cache_file(\\\"GOGJSON\\\", FileAccess.WRITE)\n\tcache_file.store_string(var_to_str(json_list))\n\nfunc _initialize() -> void:\n\tdefaults[\\\"json_path\\\"] = \\\"\\\"\n\tdefaults[\\\"branch\\\"] = \\\"\\\"\n\tdefaults[\\\"branch_password\\\"] = \\\"\\\"\n\t\n\tif json_list.is_empty():\n\t\tjson_file.add_item(\\\"List empty. Run project scan from Config tab.\\\")\n\t\tjson_file.set_item_metadata(-1, \\\"\\\")\n\t\tjson_file.disabled = true\n\telse:\n\t\tfor file in json_list:\n\t\t\tjson_file.add_item(file.get_file())\n\t\t\tjson_file.set_item_metadata(-1, file)\n\t\t\tjson_file.set_item_tooltip(-1, file)\n\nfunc _prevalidate() -> bool:\n\tif Data.global_config[\\\"pipeline_builder_path\\\"].is_empty():\n\t\terror_message = \\\"Pipeline Builder path is empty.\\\"\n\telif not FileAccess.file_exists(Data.global_config[\\\"pipeline_builder_path\\\"]):\n\t\terror_message = \\\"Pipeline Builder path does not point to any file.\\\"\n\telif Data.global_config[\\\"gog_username\\\"].is_empty():\n\t\terror_message = \\\"GOG username is empty.\\\"\n\telif Data.global_config[\\\"gog_password\\\"].is_empty():\n\t\terror_message = \\\"GOG password is empty.\\\"\n\telif Data.get_project_version().is_empty():\n\t\terror_message = \\\"Project version (application/config/version) is empty.\\\"\n\t\n\treturn error_message.is_empty()\n\nfunc _validate() -> bool:\n\tif not FileAccess.file_exists(Data.project_path.path_join(get_json_path())):\n\t\terror_message = \\\"The provided JSON file does not exist.\\\"\n\t\treturn false\n\treturn true\n\nfunc _get_command() -> String:\n\treturn Data.global_config[\\\"pipeline_builder_path\\\"]\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret: PackedStringArray\n\t\n\tret.append(\\\"build-game\\\")\n\tret.append(Data.project_path.path_join(get_json_path()))\n\t\n\tret.append(\\\"--version\\\")\n\tret.append(Data.get_project_version())\n\t\n\tret.append(\\\"--username\\\")\n\tret.append(Data.global_config[\\\"gog_username\\\"])\n\t\n\tret.append(\\\"--password\\\")\n\tret.append(Data.global_config[\\\"gog_password\\\"])\n\t\n\tif not branch.text.is_empty():\n\t\tret.append(\\\"--branch\\\")\n\t\tret.append(branch.text)\n\t\t\n\t\tif not branch_password.text.is_empty():\n\t\t\tret.append(\\\"--branch_password\\\")\n\t\t\tret.append(branch_password.text)\n\t\n\t#ret.append(\\\"--offline\\\")\n\t\n\treturn ret\n\nfunc _load():\n\tbranch.text = data[\\\"branch\\\"]\n\tbranch.text = data[\\\"branch_password\\\"]\n\t\n\tvar file: String = data[\\\"json_path\\\"]\n\tfor i in json_file.item_count:\n\t\tif json_file.get_item_metadata(i) == file:\n\t\t\tjson_file.selected = i\n\t\t\tbreak\n\nfunc _store():\n\tdata[\\\"json_path\\\"] = get_json_path()\n\tdata[\\\"branch\\\"] = branch.text\n\tdata[\\\"branch_password\\\"] = branch_password.text\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Uploads files to GOG based on the given JSON file, using Pipeline Builder.\\\",\n\t\t\\\"JSON File|File used as a base to upload.\\\",\n\t\t\\\"Branch|Branch name to upload to.\\\",\n\t\t\\\"Branch Password|Password for the branch if it's protected.\\\",\n\t]\n\nfunc get_json_path() -> String:\n\treturn json_file.get_selected_metadata()\n\"\n\n[node name=\"UploadGog\" type=\"GridContainer\"]\noffset_right = 520.0\noffset_bottom = 23.0\ncolumns = 2\nscript = SubResource(\"GDScript_a0t4t\")\nhas_static_configuration = true\nhas_sensitive_data = true\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"JSON File\"\nhorizontal_alignment = 2\n\n[node name=\"JSONFile\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Branch\"\nhorizontal_alignment = 2\n\n[node name=\"Branch\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\n\n[node name=\"Label3\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\nsize_flags_horizontal = 8\ntext = \"Branch Password\"\nhorizontal_alignment = 2\n\n[node name=\"BranchPassword\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nauto_translate_mode = 1\nlayout_mode = 2\n"
  },
  {
    "path": "Tasks/UploadItch.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://xt4wel0slqe4\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" path=\"res://Nodes/GUI/DirectorySelector.tscn\" id=\"1_qoq5m\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_5kvo4\"]\nscript/source = \"extends Task\n\n@onready var source_folder: Control = %SourceFolder\n@onready var channel: LineEdit = %Channel\n@onready var project_version: CheckBox = %ProjectVersion\n\nvar version_file: String\n\nfunc _get_task_name() -> String:\n\treturn \\\"Upload Itch\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Itch Upload (Channel: \\\\\\\"%s\\\\\\\")\\\" % channel.text\n\nfunc _initialize() -> void:\n\tdefaults[\\\"source_folder\\\"] = \\\"\\\"\n\tdefaults[\\\"channel\\\"] = \\\"\\\"\n\tdefaults[\\\"use_project_version\\\"] = false\n\nfunc _prevalidate() -> bool:\n\tversion_file = Data.local_config[\\\"itch_version_file\\\"]\n\tif not version_file.is_empty():\n\t\tversion_file = Data.project_path.path_join(version_file)\n\t\tif not FileAccess.file_exists(version_file):\n\t\t\terror_message = \\\"The provided version file does not exist.\\\"\n\t\n\tif project_version.button_pressed and Data.get_project_version().is_empty():\n\t\terror_message = \\\"Use Project Version is enabled, but project version (application/config/version) is empty.\\\"\n\t\n\tvar butler_path: String = Data.global_config[\\\"itch_butler_path\\\"]\n\tif butler_path.is_empty():\n\t\terror_message = \\\"Butler path is empty.\\\"\n\telif not FileAccess.file_exists(butler_path):\n\t\terror_message = \\\"Butler path does not point to any file.\\\"\n\telse:\n\t\tvar pipe := OS.execute_with_pipe(butler_path, [\\\"login\\\"])\n\t\tif pipe.is_empty():\n\t\t\terror_message = \\\"Butler path does not point to a valid executable.\\\"\n\t\telse:\n\t\t\tvar stdio: FileAccess = pipe[\\\"stdio\\\"]\n\t\t\tOS.delay_msec(200)\n\t\t\tvar line := stdio.get_line()\n\t\t\t\n\t\t\tif not line.contains(\\\"Your local credentials are valid!\\\"):\n\t\t\t\terror_message = \\\"Butler credentials not valid. Please login before using.\\\"\n\t\t\t\tOS.kill(pipe[\\\"pid\\\"])\n\t\n\treturn error_message.is_empty()\n\nfunc _validate() -> bool:\n\tif not DirAccess.dir_exists_absolute(Data.project_path.path_join(source_folder.text)):\n\t\terror_message = \\\"The provided source directory does not exist.\\\"\n\t\treturn false\n\treturn true\n\nfunc _get_command() -> String:\n\treturn Data.global_config[\\\"itch_butler_path\\\"]\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret: PackedStringArray\n\t\n\tret.append(\\\"push\\\")\n\tret.append(Data.project_path.path_join(source_folder.text))\n\t\n\tvar username: String = Data.global_config[\\\"itch_username\\\"]\n\tvar game_name: String = Data.local_config[\\\"itch_game_name\\\"]\n\tvar channel_name: String = channel.text\n\tif channel_name.is_empty():\n\t\tchannel_name = Data.local_config[\\\"itch_default_channel\\\"]\n\t\n\tret.append(\\\"%s/%s:%s\\\" % [username, game_name, channel_name])\n\t\n\tif project_version.button_pressed:\n\t\tret.append(\\\"--userversion\\\")\n\t\tret.append(Data.get_project_version())\n\telif not version_file.is_empty():\n\t\tret.append(\\\"--userversion-file\\\")\n\t\tret.append(version_file)\n\t\n\treturn ret\n\nfunc _load():\n\tsource_folder.text =  data[\\\"source_folder\\\"]\n\tchannel.text =  data[\\\"channel\\\"]\n\tproject_version.button_pressed =  data[\\\"use_project_version\\\"]\n\nfunc _store():\n\tdata[\\\"source_folder\\\"] = source_folder.text\n\tdata[\\\"channel\\\"] = channel.text\n\tdata[\\\"use_project_version\\\"] = project_version.button_pressed\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Uploads files to itch.io using butler. Remember to setup (login) butler before using this option.\\\",\n\t\t\\\"Source Folder|The folder that will be provided for butler. It is automatically packed into ZIP before upload.\\\",\n\t\t\\\"Channel|Channel to which the file will be uploaded. If empty, default channel from Local Config will be used.\\\",\n\t\t\\\"Use Project Version|If enabled, provides userversion argument with version from \\\\\\\"application/config/version\\\\\\\".\\\",\n\t]\n\"\n\n[node name=\"UploadItch\" type=\"GridContainer\"]\noffset_right = 536.0\noffset_bottom = 66.0\ncolumns = 2\nscript = SubResource(\"GDScript_5kvo4\")\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"Source Folder\"\nhorizontal_alignment = 2\n\n[node name=\"SourceFolder\" parent=\".\" instance=ExtResource(\"1_qoq5m\")]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nscope = 1\nmissing_mode = 1\n\n[node name=\"Label2\" type=\"Label\" parent=\".\"]\nauto_translate_mode = 1\nlayout_mode = 2\ntext = \"Channel\"\nhorizontal_alignment = 2\n\n[node name=\"Channel\" type=\"LineEdit\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Control\" type=\"Control\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"ProjectVersion\" type=\"CheckBox\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Use Project Version\"\n"
  },
  {
    "path": "Tasks/UploadSteam.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://b7iec2eyiepn8\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_ftiq7\"]\nscript/source = \"extends Task\n\n@onready var vdf_file: OptionButton = %VDFFile\n\nstatic var vdf_list: PackedStringArray\n\nfunc _get_task_name() -> String:\n\treturn \\\"Upload Steam\\\"\n\nfunc _get_execute_string() -> String:\n\treturn \\\"Steam Build (%s)\\\" % vdf_file.text\n\nstatic func _initialize_project():\n\tvar cache_file := get_cache_file(\\\"SteamVDF\\\", FileAccess.READ)\n\tif cache_file:\n\t\tvdf_list = str_to_var(cache_file.get_as_text())\n\telse:\n\t\tvdf_list.clear()\n\nstatic func _begin_project_scan() -> void:\n\tvdf_list.clear()\n\nstatic func _process_file(file: String):\n\tif file.get_extension() != \\\"vdf\\\":\n\t\treturn\n\t\n\tvar f := FileAccess.open(file, FileAccess.READ)\n\tfor line in f.get_as_text().split(\\\"\\\\n\\\"):\n\t\tif line.contains(\\\"AppBuild\\\"):\n\t\t\tvdf_list.append(file.trim_prefix(Data.project_path + \\\"/\\\"))\n\t\t\tbreak\n\nstatic func _end_project_scan() -> void:\n\tvar cache_file := get_cache_file(\\\"SteamVDF\\\", FileAccess.WRITE)\n\tcache_file.store_string(var_to_str(vdf_list))\n\nfunc _initialize() -> void:\n\tdefaults[\\\"vdf_path\\\"] = \\\"\\\"\n\t\n\tif vdf_list.is_empty():\n\t\tvdf_file.add_item(\\\"List empty. Run project scan from Config tab.\\\")\n\t\tvdf_file.set_item_metadata(-1, \\\"\\\")\n\t\tvdf_file.disabled = true\n\telse:\n\t\tfor file in vdf_list:\n\t\t\tvdf_file.add_item(file.get_file())\n\t\t\tvdf_file.set_item_metadata(-1, file)\n\t\t\tvdf_file.set_item_tooltip(-1, file)\n\nfunc _prevalidate() -> bool:\n\tif Data.global_config[\\\"steam_cmd_path\\\"].is_empty():\n\t\terror_message = \\\"Steam CMD path is empty.\\\"\n\telif not FileAccess.file_exists(Data.global_config[\\\"steam_cmd_path\\\"]):\n\t\terror_message = \\\"Steam CMD path does not point to any file.\\\"\n\telif Data.global_config[\\\"steam_username\\\"].is_empty():\n\t\terror_message = \\\"Steam username is empty.\\\"\n\telif Data.global_config[\\\"steam_password\\\"].is_empty():\n\t\terror_message = \\\"Steam password is empty.\\\"\n\t\n\treturn error_message.is_empty()\n\nfunc _validate() -> bool:\n\tif not FileAccess.file_exists(Data.project_path.path_join(get_vdf_path())):\n\t\terror_message = \\\"The provided VDF file does not exist.\\\"\n\t\treturn false\n\treturn true\n\nfunc _get_command() -> String:\n\treturn Data.global_config[\\\"steam_cmd_path\\\"]\n\nfunc _get_arguments() -> PackedStringArray:\n\tvar ret: PackedStringArray\n\t\n\tret.append(\\\"+login\\\")\n\tret.append(Data.global_config[\\\"steam_username\\\"])\n\tret.append(Data.global_config[\\\"steam_password\\\"])\n\t\n\tret.append(\\\"+run_app_build\\\")\n\tret.append(Data.project_path.path_join(get_vdf_path()))\n\t\n\tret.append(\\\"+quit\\\")\n\t\n\treturn ret\n\nfunc _load():\n\tvar file: String = data[\\\"vdf_path\\\"]\n\t\n\tfor i in vdf_file.item_count:\n\t\tif vdf_file.get_item_metadata(i) == file:\n\t\t\tvdf_file.selected = i\n\t\t\tbreak\n\nfunc _store():\n\tdata[\\\"vdf_path\\\"] = get_vdf_path()\n\nfunc _get_task_info() -> PackedStringArray:\n\treturn [\n\t\t\\\"Uploads files to Steam based on the given VDF file, using steamcmd.exe.\\\",\n\t\t\\\"VDF File|File used as a base to upload. Only AppBuild VDF files are supported.\\\",\n\t]\n\nfunc get_vdf_path() -> String:\n\treturn vdf_file.get_selected_metadata()\n\"\n\n[node name=\"UploadSteam\" type=\"GridContainer\"]\noffset_right = 522.0\noffset_bottom = 23.0\ncolumns = 2\nscript = SubResource(\"GDScript_ftiq7\")\nhas_static_configuration = true\nhas_sensitive_data = true\n\n[node name=\"Label\" type=\"Label\" parent=\".\"]\ncustom_minimum_size = Vector2(150, 0)\nlayout_mode = 2\ntext = \"VDF File\"\nhorizontal_alignment = 2\n\n[node name=\"VDFFile\" type=\"OptionButton\" parent=\".\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\n"
  },
  {
    "path": "Tests/GutConfig.json",
    "content": "{\n \"background_color\": \"262626ff\",\n \"compact_mode\": false,\n \"configured_dirs\": [\n  \"res://Tests\"\n ],\n \"dirs\": [\n  \"res://Tests\"\n ],\n \"disable_colors\": false,\n \"double_strategy\": 1,\n \"errors_do_not_cause_failure\": false,\n \"font_color\": \"ccccccff\",\n \"font_name\": \"CourierPrime\",\n \"font_size\": 16,\n \"gut_on_top\": true,\n \"hide_orphans\": false,\n \"ignore_pause\": false,\n \"include_subdirs\": false,\n \"junit_xml_file\": \"\",\n \"junit_xml_timestamp\": false,\n \"log_level\": 1,\n \"opacity\": 100,\n \"paint_after\": 0.1,\n \"post_run_script\": \"\",\n \"pre_run_script\": \"\",\n \"prefix\": \"Test\",\n \"should_exit\": false,\n \"should_exit_on_success\": false,\n \"should_maximize\": false,\n \"suffix\": \".gd\"\n}"
  },
  {
    "path": "Tests/Projects/.gdignore",
    "content": ""
  },
  {
    "path": "Tests/Projects/TestProject1/DeepDir/DirFile1.txt",
    "content": "DirFile1\n"
  },
  {
    "path": "Tests/Projects/TestProject1/DeepDir/DirFile2.txt",
    "content": "DirFile2\n"
  },
  {
    "path": "Tests/Projects/TestProject1/DeepDir/SubDir/SubDirFile1.txt",
    "content": "SubDirFile1\n"
  },
  {
    "path": "Tests/Projects/TestProject1/EmptyDir/.gdignore",
    "content": ""
  },
  {
    "path": "Tests/Projects/TestProject1/File1.txt",
    "content": "File1\n"
  },
  {
    "path": "Tests/Projects/TestProject1/MixedDir/MdFile1.md",
    "content": "MdFile1\n"
  },
  {
    "path": "Tests/Projects/TestProject1/MixedDir/MdFile2.md",
    "content": "MdFile2\n"
  },
  {
    "path": "Tests/Projects/TestProject1/MixedDir/TxtFile1.txt",
    "content": "TxtFile1\n"
  },
  {
    "path": "Tests/Projects/TestProject1/MixedDir/TxtFile2.txt",
    "content": "TxtFile2\n"
  },
  {
    "path": "Tests/Projects/TestProject1/project.godot",
    "content": "; Engine configuration file.\n; It's best edited using the editor UI and not directly,\n; since the parameters that go here are not all obvious.\n;\n; Format:\n;   [section] ; section goes between []\n;   param=value ; assign values to parameters\n\nconfig_version=5\n\n[application]\n\nconfig/name=\"TestProject1\"\nconfig/features=PackedStringArray(\"4.3\", \"Forward Plus\")\n"
  },
  {
    "path": "Tests/Projects/TestProject1/project_builds_config.txt",
    "content": "{\n\"epic_artifact_id\": \"\",\n\"epic_cloud_dir\": \"\",\n\"epic_product_id\": \"\",\n\"godot_path\": \"\",\n\"itch_default_channel\": \"\",\n\"itch_game_name\": \"\",\n\"itch_version_file\": \"\",\n\"routines\": Array[Dictionary]([{\n\"name\": \"Copy Files Test\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"File1Copy.txt\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"EmptyDir/File1Copy.txt\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"DeepDir\",\n\"target_path\": \"EmptyDir/DirCopyRecursive\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": false,\n\"source_path\": \"DeepDir\",\n\"target_path\": \"EmptyDir/DirCopyNotRecursive\"\n},\n\"scene\": \"CopyFiles\"\n}])\n}, {\n\"name\": \"Clear Directory Files Test\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"exclude_files\": \"\",\n\"include_files\": \"\",\n\"target_directory\": \"DeepDir\"\n},\n\"scene\": \"ClearDirectory\"\n}])\n}, {\n\"name\": \"Pack ZIP Test\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"destination\": \"DeepDir.zip\",\n\"exclude_files\": \"\",\n\"include_files\": \"\",\n\"source\": \"DeepDir\"\n},\n\"scene\": \"PackZIP\"\n}, {\n\"data\": {\n\"destination\": \"IncludeBlob.zip\",\n\"exclude_files\": \"\",\n\"include_files\": \"*.md\",\n\"source\": \"MixedDir\"\n},\n\"scene\": \"PackZIP\"\n}, {\n\"data\": {\n\"destination\": \"Exclude.zip\",\n\"exclude_files\": \"SubDirFile1.txt\",\n\"include_files\": \"\",\n\"source\": \"DeepDir\"\n},\n\"scene\": \"PackZIP\"\n}, {\n\"data\": {\n\"destination\": \"IncludeExclude.zip\",\n\"exclude_files\": \"TxtFile1.txt\",\n\"include_files\": \"*.txt\",\n\"source\": \"MixedDir\"\n},\n\"scene\": \"PackZIP\"\n}])\n}, {\n\"name\": \"Sub-Routine Test\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Run Sub-Routines\"\n},\n\"scene\": \"SubRoutine\"\n}])\n}, {\n\"name\": \"Run Sub-Routines\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Copy 1 and 2\"\n},\n\"scene\": \"SubRoutine\"\n}, {\n\"data\": {\n\"routine\": \"Copy 3 and 4\"\n},\n\"scene\": \"SubRoutine\"\n}])\n}, {\n\"name\": \"Copy 1 and 2\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"Copy1.txt\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"Copy2.txt\"\n},\n\"scene\": \"CopyFiles\"\n}])\n}, {\n\"name\": \"Copy 3 and 4\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"Copy3.txt\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"File1.txt\",\n\"target_path\": \"Copy4.txt\"\n},\n\"scene\": \"CopyFiles\"\n}])\n}, {\n\"name\": \"Cyclic Sub-Routine 1\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Cyclic Sub-Routine 2\"\n},\n\"scene\": \"SubRoutine\"\n}])\n}, {\n\"name\": \"Cyclic Sub-Routine 2\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Cyclic Sub-Routine 1\"\n},\n\"scene\": \"SubRoutine\"\n}])\n}, {\n\"name\": \"Cyclic Sub-Routine 3\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Cyclic Sub-Routine 1\"\n},\n\"scene\": \"SubRoutine\"\n}])\n}]),\n\"templates\": Array[Dictionary]([])\n}"
  },
  {
    "path": "Tests/TestExecution.gd",
    "content": "extends GutTest\n\nconst PROJECTS := {\n\t1: \"res://Tests/Projects/TestProject1/\",\n}\nconst ROUTINES := {\n\t# TestProject1\n\t\"copy_files\": 0,\n\t\"clear_directory_files\": 1,\n\t\"pack_zip\": 2,\n\t\"sub_routine\": 3,\n\t\"cyclic_sub_routine_1\": 7,\n\t\"cyclic_sub_routine_2\": 9,\n}\nconst EXECUTION_TIMEOUT := 20.0\nconst Scene := preload(\"res://Scenes/Execution.tscn\")\nvar scene: Node\nvar original_exec_delay\n\nfunc before_all():\n\toriginal_exec_delay = Data.global_config[\"execution_delay\"]\n\tData.global_config[\"execution_delay\"] = 0\n\nfunc before_each():\n\tscene = Scene.instantiate()\n\nfunc after_each():\n\tscene.free()\n\nfunc after_all():\n\tData.global_config[\"execution_delay\"] = original_exec_delay\n\nfunc test_copy_files():\n\t# Check assumptions\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"File1.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"File1Copy.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/File1Copy.txt\"))\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"DeepDir\"))\n\tassert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive\"))\n\tassert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive\"))\n\t\n\t# Setup\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.copy_files]\n\t\n\t# Execute\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"File1.txt\"))\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"DeepDir\"))\n\t\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"File1Copy.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/File1Copy.txt\"))\n\t\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/DirFile1.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/DirFile2.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/SubDir/SubDirFile1.txt\"))\n\t\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/DirFile1.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/DirFile2.txt\"))\n\tassert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/SubDir\"))\n\n\t# Cleanup\n\tDirAccess.remove_absolute(PROJECTS[1] + \"File1Copy.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/File1Copy.txt\")\n\t\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/DirFile1.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/DirFile2.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/SubDir/SubDirFile1.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive/SubDir\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyRecursive\")\n\t\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/DirFile1.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/DirFile2.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/SubDir/SubDirFile1.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive/SubDir\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"EmptyDir/DirCopyNotRecursive\")\n\nfunc test_clear_directory_files():\n\t# Check assumptions\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/DirFile1.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/DirFile2.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/SubDir/SubDirFile1.txt\"))\n\t\n\t# Setup\n\tDirAccess.copy_absolute(PROJECTS[1] + \"DeepDir/DirFile1.txt\", PROJECTS[1] + \"EmptyDir/DirFile1.txt\")\n\tDirAccess.copy_absolute(PROJECTS[1] + \"DeepDir/DirFile2.txt\", PROJECTS[1] + \"EmptyDir/DirFile2.txt\")\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.clear_directory_files]\n\t\n\t# Execute\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/DirFile1.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/DirFile2.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"DeepDir/SubDir/SubDirFile1.txt\"))\n\n\t# Cleanup\n\tDirAccess.rename_absolute(PROJECTS[1] + \"EmptyDir/DirFile1.txt\", PROJECTS[1] + \"DeepDir/DirFile1.txt\")\n\tDirAccess.rename_absolute(PROJECTS[1] + \"EmptyDir/DirFile2.txt\", PROJECTS[1] + \"DeepDir/DirFile2.txt\")\n\nfunc test_pack_zip():\n\t# Check assumptions\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"DeepDir\"))\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"MixedDir\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"DeepDir.zip\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"IncludeBlob.zip\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"Exclude.zip\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"IncludeExclude.zip\"))\n\t\n\t# Setup\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.pack_zip]\n\t\n\t# Execute\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"DeepDir\"))\n\tassert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + \"MixedDir\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"DeepDir.zip\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"IncludeBlob.zip\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"Exclude.zip\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"IncludeExclude.zip\"))\n\t\n\tvar reader := ZIPReader.new()\n\tvar error := reader.open(PROJECTS[1] + \"DeepDir.zip\")\n\tvar files := reader.get_files()\n\treader.close()\n\tassert_true(error == OK)\n\tassert_true(files.has(\"DirFile1.txt\"))\n\tassert_true(files.has(\"DirFile2.txt\"))\n\tassert_true(files.has(\"SubDir/SubDirFile1.txt\"))\n\t\n\treader = ZIPReader.new()\n\terror = reader.open(PROJECTS[1] + \"IncludeBlob.zip\")\n\tfiles = reader.get_files()\n\treader.close()\n\tassert_true(error == OK)\n\tassert_false(files.has(\"TxtFile1.txt\"))\n\tassert_false(files.has(\"TxtFile2.txt\"))\n\tassert_true(files.has(\"MdFile1.md\"))\n\tassert_true(files.has(\"MdFile2.md\"))\n\t\n\treader = ZIPReader.new()\n\terror = reader.open(PROJECTS[1] + \"Exclude.zip\")\n\tfiles = reader.get_files()\n\treader.close()\n\tassert_true(error == OK)\n\tassert_true(files.has(\"DirFile1.txt\"))\n\tassert_true(files.has(\"DirFile2.txt\"))\n\tassert_false(files.has(\"SubDir/SubDirFile1.txt\"))\n\t\n\treader = ZIPReader.new()\n\terror = reader.open(PROJECTS[1] + \"IncludeExclude.zip\")\n\tfiles = reader.get_files()\n\treader.close()\n\tassert_true(error == OK)\n\tassert_false(files.has(\"TxtFile1.txt\"))\n\tassert_true(files.has(\"TxtFile2.txt\"))\n\tassert_false(files.has(\"MdFile1.md\"))\n\tassert_false(files.has(\"MdFile2.md\"))\n\n\t# Cleanup\n\tDirAccess.remove_absolute(PROJECTS[1] + \"DeepDir.zip\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"IncludeBlob.zip\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"Exclude.zip\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"IncludeExclude.zip\")\n\nfunc test_sub_routine():\n\t# Check assumptions\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"File1.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"Copy1.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"Copy2.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"Copy3.txt\"))\n\tassert_false(FileAccess.file_exists(PROJECTS[1] + \"Copy4.txt\"))\n\t\n\t# Setup\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.sub_routine]\n\t\n\t# Execute\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"File1.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"Copy1.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"Copy2.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"Copy3.txt\"))\n\tassert_true(FileAccess.file_exists(PROJECTS[1] + \"Copy4.txt\"))\n\n\t# Cleanup\n\tDirAccess.remove_absolute(PROJECTS[1] + \"Copy1.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"Copy2.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"Copy3.txt\")\n\tDirAccess.remove_absolute(PROJECTS[1] + \"Copy4.txt\")\n\nfunc test_cyclic_sub_routine_1():\n\t# Setup\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.cyclic_sub_routine_1]\n\t\n\t# Execute\n\twatch_signals(scene)\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_signal_emitted(scene, \"finished\")\n\nfunc test_cyclic_sub_routine_2():\n\t# Setup\n\tData.load_project(PROJECTS[1])\n\tData.current_routine = Data.routines[ROUTINES.cyclic_sub_routine_2]\n\t\n\t# Execute\n\twatch_signals(scene)\n\tadd_child.call_deferred(scene)\n\tawait wait_for_signal(scene.finished, EXECUTION_TIMEOUT)\n\t\n\t# Check results\n\tassert_signal_emitted(scene, \"finished\")\n"
  },
  {
    "path": "Tests/TestExecution.gd.uid",
    "content": "uid://dbnpvtbcdms7l\n"
  },
  {
    "path": "addons/Prefab/Prefab.gd",
    "content": "extends PackedScene\nclass_name Prefab\n\nstatic func create(node: Node, deferred_free := false) -> Prefab:\n\tassert(node, \"Invalid node provided.\")\n\t\n\tvar to_check := node.get_children()\n\twhile not to_check.is_empty():\n\t\tvar sub: Node = to_check.pop_back()\n\t\tif sub.owner == null:\n\t\t\tcontinue\n\t\t\n\t\tto_check.append_array(sub.get_children())\n\t\tsub.owner = node\n\t\n\tvar prefab := Prefab.new()\n\tprefab.pack(node)\n\t\n\tif deferred_free:\n\t\tnode.queue_free()\n\telse:\n\t\tnode.free()\n\t\n\treturn prefab\n"
  },
  {
    "path": "addons/Prefab/Prefab.gd.uid",
    "content": "uid://c3i8g8t027eyi\n"
  },
  {
    "path": "addons/ProjectBuilder/ProjectBuilderPlugin.gd",
    "content": "@tool\nextends EditorPlugin\n\nconst CONFIG_SETTING = \"_project_builder_config_path\"\n\nvar popup: PopupMenu\nvar cached_routine_list: PackedStringArray\n\nfunc _enter_tree() -> void:\n\tpopup = PopupMenu.new()\n\tpopup.index_pressed.connect(on_popup_action)\n\trefresh_popup()\n\tadd_tool_submenu_item(\"Project Builder\", popup)\n\t\n\tEditorInterface.get_command_palette().add_command(\"Run Project Builder\", \"project_builder/run_project_builder\", run_project_builder)\n\tfor routine in get_routine_list():\n\t\tEditorInterface.get_command_palette().add_command(\"Execute: \" + routine, \"project_builder/\" + routine, run_project_builder.bind(routine))\n\t\n\tif ProjectSettings.has_setting(\"addons/project_builder/config_path\"): # compat\n\t\tProjectSettings.set_setting(CONFIG_SETTING, ProjectSettings.get_setting(\"addons/project_builder/config_path\"))\n\t\tProjectSettings.set_setting(\"addons/project_builder/config_path\", null)\n\t\tProjectSettings.save()\n\telif not ProjectSettings.has_setting(CONFIG_SETTING):\n\t\tProjectSettings.set_setting(CONFIG_SETTING, \"res://project_builds_config.txt\")\n\tProjectSettings.set_as_internal(CONFIG_SETTING, true)\n\t\n\tProjectSettings.add_property_info({ \"name\": CONFIG_SETTING, \"type\": TYPE_STRING, \"hint\": PROPERTY_HINT_SAVE_FILE })\n\nfunc _exit_tree() -> void:\n\tremove_tool_menu_item(\"Project Builder\")\n\tEditorInterface.get_command_palette().remove_command(\"project_builder/run_project_builder\")\n\tfor routine in get_routine_list():\n\t\tEditorInterface.get_command_palette().remove_command(\"project_builder/\" + routine)\n\nfunc refresh_popup():\n\tpopup.clear()\n\tcached_routine_list.clear()\n\t\n\tpopup.add_item(\"Run Project Builder\")\n\tpopup.add_item(\"Refresh Routine List\")\n\t\n\tvar routine_list := get_routine_list()\n\tif routine_list.is_empty():\n\t\tpopup.add_separator(\"No Routines\")\n\telse:\n\t\tpopup.add_separator(\"Execute Routine\")\n\t\t\n\t\tfor routine in routine_list:\n\t\t\tpopup.add_item(routine)\n\nfunc get_routine_list() -> PackedStringArray:\n\tif not cached_routine_list.is_empty():\n\t\treturn cached_routine_list\n\t\n\tvar routine_list: PackedStringArray\n\t\n\tvar project_builds_config := FileAccess.open(ProjectSettings.get_setting(CONFIG_SETTING), FileAccess.READ)\n\tif project_builds_config:\n\t\tvar data: Dictionary = str_to_var(project_builds_config.get_as_text())\n\t\tfor routine in data[\"routines\"]:\n\t\t\troutine_list.append(routine[\"name\"])\n\t\n\tcached_routine_list = routine_list\n\treturn routine_list\n\nfunc on_popup_action(idx: int):\n\tmatch idx:\n\t\t0:\n\t\t\trun_project_builder()\n\t\t1:\n\t\t\trefresh_popup()\n\t\t_:\n\t\t\trun_project_builder(popup.get_item_text(idx))\n\nfunc run_project_builder(routine := \"\"):\n\tvar project_builds_config := FileAccess.open(OS.get_user_data_dir().get_base_dir().path_join(\"Godot Project Builder/project_builds_config.txt\"), FileAccess.READ)\n\tif not project_builds_config:\n\t\tOS.alert(\"Project Builder config file not found. Make sure you run Project Builder directly at least once.\", \"Something went wrong\")\n\t\treturn\n\t\n\tvar data: Dictionary = str_to_var(project_builds_config.get_as_text())\n\tvar project_path: String = data[\"project_builder_path\"]\n\t\n\tif not DirAccess.dir_exists_absolute(project_path):\n\t\tOS.alert(\"Project Builder project directory found. Make sure you run Project Builder directly at least once.\", \"Something went very wrong\")\n\t\treturn\n\t\n\tvar arguments: PackedStringArray\n\tif not project_path.is_empty():\n\t\targuments.append_array([\"--path\", project_path])\n\t\n\targuments.append_array([\"--\", \"--open-project\", ProjectSettings.globalize_path(\"res://\").trim_suffix(\"/\")])\n\tif not routine.is_empty():\n\t\targuments.append_array([\"--execute-routine\", routine])\n\t\n\tvar executable_path: String = data[\"project_builder_executable\"]\n\tif FileAccess.file_exists(executable_path):\n\t\tOS.create_process(executable_path, arguments)\n\telse:\n\t\tOS.create_instance(arguments)\n"
  },
  {
    "path": "addons/ProjectBuilder/ProjectBuilderPlugin.gd.uid",
    "content": "uid://sef5h7ohaqw0\n"
  },
  {
    "path": "addons/ProjectBuilder/plugin.cfg",
    "content": "[plugin]\n\nname=\"Project Builder Plugin\"\ndescription=\"Helper addon to start Project Builder from within the project.\"\nauthor=\"KoBeWi\"\nversion=\"1.1.1\"\nscript=\"ProjectBuilderPlugin.gd\"\n"
  },
  {
    "path": "addons/gut/GutScene.gd",
    "content": "extends Node2D\n# ##############################################################################\n# This is a wrapper around the normal and compact gui controls and serves as\n# the interface between gut.gd and the gui.  The GutRunner creates an instance\n# of this and then this takes care of managing the different GUI controls.\n# ##############################################################################\n@onready var _normal_gui = $Normal\n@onready var _compact_gui = $Compact\n\nvar gut = null :\n\tset(val):\n\t\tgut = val\n\t\t_set_gut(val)\n\n\nfunc _ready():\n\t_normal_gui.switch_modes.connect(use_compact_mode.bind(true))\n\t_compact_gui.switch_modes.connect(use_compact_mode.bind(false))\n\n\t_normal_gui.set_title(\"GUT\")\n\t_compact_gui.set_title(\"GUT\")\n\n\t_normal_gui.align_right()\n\t_compact_gui.to_bottom_right()\n\n\tuse_compact_mode(false)\n\n\tif(get_parent() == get_tree().root):\n\t\t_test_running_setup()\n\nfunc _test_running_setup():\n\tset_font_size(100)\n\t_normal_gui.get_textbox().text = \"hello world, how are you doing?\"\n\n# ------------------------\n# Private\n# ------------------------\nfunc _set_gut(val):\n\tif(_normal_gui.get_gut() == val):\n\t\treturn\n\t_normal_gui.set_gut(val)\n\t_compact_gui.set_gut(val)\n\n\tval.start_run.connect(_on_gut_start_run)\n\tval.end_run.connect(_on_gut_end_run)\n\tval.start_pause_before_teardown.connect(_on_gut_pause)\n\tval.end_pause_before_teardown.connect(_on_pause_end)\n\nfunc _set_both_titles(text):\n\t_normal_gui.set_title(text)\n\t_compact_gui.set_title(text)\n\n\n# ------------------------\n# Events\n# ------------------------\nfunc _on_gut_start_run():\n\t_set_both_titles('Running')\n\nfunc _on_gut_end_run():\n\t_set_both_titles('Finished')\n\nfunc _on_gut_pause():\n\t_set_both_titles('-- Paused --')\n\nfunc _on_pause_end():\n\t_set_both_titles('Running')\n\n\n# ------------------------\n# Public\n# ------------------------\nfunc get_textbox():\n\treturn _normal_gui.get_textbox()\n\n\nfunc set_font_size(new_size):\n\tvar rtl = _normal_gui.get_textbox()\n\n\trtl.set('theme_override_font_sizes/bold_italics_font_size', new_size)\n\trtl.set('theme_override_font_sizes/bold_font_size', new_size)\n\trtl.set('theme_override_font_sizes/italics_font_size', new_size)\n\trtl.set('theme_override_font_sizes/normal_font_size', new_size)\n\n\nfunc set_font(font_name):\n\t_set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name)\n\n\nfunc _set_font(rtl, font_name, custom_name):\n\tif(font_name == null):\n\t\trtl.remove_theme_font_override(custom_name)\n\telse:\n\t\tvar dyn_font = FontFile.new()\n\t\tdyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')\n\t\trtl.add_theme_font_override(custom_name, dyn_font)\n\n\nfunc _set_all_fonts_in_rtl(rtl, base_name):\n\tif(base_name == 'Default'):\n\t\t_set_font(rtl, null, 'normal_font')\n\t\t_set_font(rtl, null, 'bold_font')\n\t\t_set_font(rtl, null, 'italics_font')\n\t\t_set_font(rtl, null, 'bold_italics_font')\n\telse:\n\t\t_set_font(rtl, base_name + '-Regular', 'normal_font')\n\t\t_set_font(rtl, base_name + '-Bold', 'bold_font')\n\t\t_set_font(rtl, base_name + '-Italic', 'italics_font')\n\t\t_set_font(rtl, base_name + '-BoldItalic', 'bold_italics_font')\n\n\nfunc set_default_font_color(color):\n\t_normal_gui.get_textbox().set('custom_colors/default_color', color)\n\n\nfunc set_background_color(color):\n\t_normal_gui.set_bg_color(color)\n\n\nfunc use_compact_mode(should=true):\n\t_compact_gui.visible = should\n\t_normal_gui.visible = !should\n\n\nfunc set_opacity(val):\n\t_normal_gui.modulate.a = val\n\t_compact_gui.modulate.a = val\n\nfunc set_title(text):\n\t_set_both_titles(text)\n"
  },
  {
    "path": "addons/gut/GutScene.gd.uid",
    "content": "uid://dxo1rbug4pkeq\n"
  },
  {
    "path": "addons/gut/GutScene.tscn",
    "content": "[gd_scene load_steps=4 format=3 uid=\"uid://m28heqtswbuq\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dxo1rbug4pkeq\" path=\"res://addons/gut/GutScene.gd\" id=\"1_b4m8y\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://duxblir3vu8x7\" path=\"res://addons/gut/gui/NormalGui.tscn\" id=\"2_j6ywb\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://cnqqdfsn80ise\" path=\"res://addons/gut/gui/MinGui.tscn\" id=\"3_3glw1\"]\n\n[node name=\"GutScene\" type=\"Node2D\"]\nscript = ExtResource(\"1_b4m8y\")\n\n[node name=\"Normal\" parent=\".\" instance=ExtResource(\"2_j6ywb\")]\n\n[node name=\"Compact\" parent=\".\" instance=ExtResource(\"3_3glw1\")]\noffset_left = 5.0\noffset_top = 273.0\noffset_right = 265.0\noffset_bottom = 403.0\n"
  },
  {
    "path": "addons/gut/LICENSE.md",
    "content": "The MIT License (MIT)\n=====================\n\nCopyright (c) 2018 Tom \"Butch\" Wesley\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "addons/gut/UserFileViewer.gd",
    "content": "extends Window\n\n@onready var rtl = $TextDisplay/RichTextLabel\n\nfunc _get_file_as_text(path):\n\tvar to_return = null\n\tvar f = FileAccess.open(path, FileAccess.READ)\n\tif(f != null):\n\t\tto_return = f.get_as_text()\n\telse:\n\t\tto_return = str('ERROR:  Could not open file.  Error code ', FileAccess.get_open_error())\n\treturn to_return\n\nfunc _ready():\n\trtl.clear()\n\nfunc _on_OpenFile_pressed():\n\t$FileDialog.popup_centered()\n\nfunc _on_FileDialog_file_selected(path):\n\tshow_file(path)\n\nfunc _on_Close_pressed():\n\tself.hide()\n\nfunc show_file(path):\n\tvar text = _get_file_as_text(path)\n\tif(text == ''):\n\t\ttext = '<Empty File>'\n\trtl.set_text(text)\n\tself.window_title = path\n\nfunc show_open():\n\tself.popup_centered()\n\t$FileDialog.popup_centered()\n\nfunc get_rich_text_label():\n\treturn $TextDisplay/RichTextLabel\n\nfunc _on_Home_pressed():\n\trtl.scroll_to_line(0)\n\nfunc _on_End_pressed():\n\trtl.scroll_to_line(rtl.get_line_count() -1)\n\nfunc _on_Copy_pressed():\n\treturn\n\t# OS.clipboard = rtl.text\n\nfunc _on_file_dialog_visibility_changed():\n\tif rtl.text.length() == 0 and not $FileDialog.visible:\n\t\tself.hide()\n"
  },
  {
    "path": "addons/gut/UserFileViewer.gd.uid",
    "content": "uid://or7j8eak2s7t\n"
  },
  {
    "path": "addons/gut/UserFileViewer.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://bsm7wtt1gie4v\"]\n\n[ext_resource type=\"Script\" uid=\"uid://or7j8eak2s7t\" path=\"res://addons/gut/UserFileViewer.gd\" id=\"1\"]\n\n[node name=\"UserFileViewer\" type=\"Window\"]\nexclusive = true\nscript = ExtResource(\"1\")\n\n[node name=\"FileDialog\" type=\"FileDialog\" parent=\".\"]\naccess = 1\nshow_hidden_files = true\n\n[node name=\"TextDisplay\" type=\"ColorRect\" parent=\".\"]\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = 8.0\noffset_right = -10.0\noffset_bottom = -65.0\ncolor = Color(0.2, 0.188235, 0.188235, 1)\n\n[node name=\"RichTextLabel\" type=\"RichTextLabel\" parent=\"TextDisplay\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\nfocus_mode = 2\ntext = \"In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design.\n\nLorem ipsum is typically a corrupted version of De finibus bonorum et malorum, a first-century BCE text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical, improper Latin.\n\nVersions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. Lorem ipsum was introduced to the digital world in the mid-1980s when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors including Pages and Microsoft Word have since adopted Lorem ipsum as well.\"\nselection_enabled = true\n\n[node name=\"OpenFile\" type=\"Button\" parent=\".\"]\nanchors_preset = 3\nanchor_left = 1.0\nanchor_top = 1.0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = -158.0\noffset_top = -50.0\noffset_right = -84.0\noffset_bottom = -30.0\ntext = \"Open File\"\n\n[node name=\"Home\" type=\"Button\" parent=\".\"]\nanchors_preset = 3\nanchor_left = 1.0\nanchor_top = 1.0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = -478.0\noffset_top = -50.0\noffset_right = -404.0\noffset_bottom = -30.0\ntext = \"Home\"\n\n[node name=\"Copy\" type=\"Button\" parent=\".\"]\nanchors_preset = 2\nanchor_top = 1.0\nanchor_bottom = 1.0\noffset_left = 160.0\noffset_top = -50.0\noffset_right = 234.0\noffset_bottom = -30.0\ntext = \"Copy\"\n\n[node name=\"End\" type=\"Button\" parent=\".\"]\nanchors_preset = 3\nanchor_left = 1.0\nanchor_top = 1.0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = -318.0\noffset_top = -50.0\noffset_right = -244.0\noffset_bottom = -30.0\ntext = \"End\"\n\n[node name=\"Close\" type=\"Button\" parent=\".\"]\nanchors_preset = 2\nanchor_top = 1.0\nanchor_bottom = 1.0\noffset_left = 10.0\noffset_top = -50.0\noffset_right = 80.0\noffset_bottom = -30.0\ntext = \"Close\"\n\n[connection signal=\"file_selected\" from=\"FileDialog\" to=\".\" method=\"_on_FileDialog_file_selected\"]\n[connection signal=\"visibility_changed\" from=\"FileDialog\" to=\".\" method=\"_on_file_dialog_visibility_changed\"]\n[connection signal=\"pressed\" from=\"OpenFile\" to=\".\" method=\"_on_OpenFile_pressed\"]\n[connection signal=\"pressed\" from=\"Home\" to=\".\" method=\"_on_Home_pressed\"]\n[connection signal=\"pressed\" from=\"Copy\" to=\".\" method=\"_on_Copy_pressed\"]\n[connection signal=\"pressed\" from=\"End\" to=\".\" method=\"_on_End_pressed\"]\n[connection signal=\"pressed\" from=\"Close\" to=\".\" method=\"_on_Close_pressed\"]\n"
  },
  {
    "path": "addons/gut/autofree.gd",
    "content": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# Class used to keep track of objects to be freed and utilities to free them.\n# ##############################################################################\nvar _to_free = []\nvar _to_queue_free = []\n\nfunc add_free(thing):\n\tif(typeof(thing) == TYPE_OBJECT):\n\t\tif(!thing is RefCounted):\n\t\t\t_to_free.append(thing)\n\nfunc add_queue_free(thing):\n\t_to_queue_free.append(thing)\n\nfunc get_queue_free_count():\n\treturn _to_queue_free.size()\n\nfunc get_free_count():\n\treturn _to_free.size()\n\nfunc free_all():\n\tfor i in range(_to_free.size()):\n\t\tif(is_instance_valid(_to_free[i])):\n\t\t\t_to_free[i].free()\n\t_to_free.clear()\n\n\tfor i in range(_to_queue_free.size()):\n\t\tif(is_instance_valid(_to_queue_free[i])):\n\t\t\t_to_queue_free[i].queue_free()\n\t_to_queue_free.clear()\n"
  },
  {
    "path": "addons/gut/autofree.gd.uid",
    "content": "uid://u4sats0gvfeu\n"
  },
  {
    "path": "addons/gut/awaiter.gd",
    "content": "extends Node\n\nsignal timeout\nsignal wait_started\n\nvar _wait_time := 0.0\nvar _wait_frames := 0\nvar _signal_to_wait_on = null\n\nvar _predicate_function_waiting_to_be_true = null\nvar _predicate_time_between := 0.0\nvar _predicate_time_between_elpased := 0.0\n\nvar _did_last_wait_timeout = false\nvar did_last_wait_timeout = false :\n\tget: return _did_last_wait_timeout\n\tset(val): push_error(\"Cannot set did_last_wait_timeout\")\n\nvar _elapsed_time := 0.0\nvar _elapsed_frames := 0\n\n\nfunc _physics_process(delta):\n\tif(_wait_time != 0.0):\n\t\t_elapsed_time += delta\n\t\tif(_elapsed_time >= _wait_time):\n\t\t\t_end_wait()\n\n\tif(_wait_frames != 0):\n\t\t_elapsed_frames += 1\n\t\tif(_elapsed_frames >= _wait_frames):\n\t\t\t_end_wait()\n\n\tif(_predicate_function_waiting_to_be_true != null):\n\t\t_predicate_time_between_elpased += delta\n\t\tif(_predicate_time_between_elpased >= _predicate_time_between):\n\t\t\t_predicate_time_between_elpased = 0.0\n\t\t\tvar result = _predicate_function_waiting_to_be_true.call()\n\t\t\tif(typeof(result) == TYPE_BOOL and result):\n\t\t\t\t_end_wait()\n\n\nfunc _end_wait():\n\t# Check for time before checking for frames so that the extra frames added\n\t# when waiting on a signal do not cause a false negative for timing out.\n\tif(_wait_time > 0):\n\t\t_did_last_wait_timeout = _elapsed_time >= _wait_time\n\telif(_wait_frames > 0):\n\t\t_did_last_wait_timeout = _elapsed_frames >= _wait_frames\n\n\tif(_signal_to_wait_on != null and _signal_to_wait_on.is_connected(_signal_callback)):\n\t\t_signal_to_wait_on.disconnect(_signal_callback)\n\n\t_wait_time = 0.0\n\t_wait_frames = 0\n\t_signal_to_wait_on = null\n\t_predicate_function_waiting_to_be_true = null\n\t_elapsed_time = 0.0\n\t_elapsed_frames = 0\n\ttimeout.emit()\n\n\nconst ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'\nfunc _signal_callback(\n\t\t_arg1=ARG_NOT_SET, _arg2=ARG_NOT_SET, _arg3=ARG_NOT_SET,\n\t\t_arg4=ARG_NOT_SET, _arg5=ARG_NOT_SET, _arg6=ARG_NOT_SET,\n\t\t_arg7=ARG_NOT_SET, _arg8=ARG_NOT_SET, _arg9=ARG_NOT_SET):\n\n\t_signal_to_wait_on.disconnect(_signal_callback)\n\t# DO NOT _end_wait here.  For other parts of the test to get the signal that\n\t# was waited on, we have to wait for a couple more frames.  For example, the\n\t# signal_watcher doesn't get the signal in time if we don't do this.\n\t_wait_frames = 2\n\nfunc wait_seconds(x):\n\t_did_last_wait_timeout = false\n\t_wait_time = x\n\twait_started.emit()\n\n\nfunc wait_frames(x):\n\t_did_last_wait_timeout = false\n\t_wait_frames = x\n\twait_started.emit()\n\n\nfunc wait_for_signal(the_signal, max_time):\n\t_did_last_wait_timeout = false\n\tthe_signal.connect(_signal_callback)\n\t_signal_to_wait_on = the_signal\n\t_wait_time = max_time\n\twait_started.emit()\n\n\nfunc wait_until(predicate_function: Callable, max_time, time_between_calls:=0.0):\n\t_predicate_time_between = time_between_calls\n\t_predicate_function_waiting_to_be_true = predicate_function\n\t_predicate_time_between_elpased = 0.0\n\t_did_last_wait_timeout = false\n\t_wait_time = max_time\n\twait_started.emit()\n\n\nfunc is_waiting():\n\treturn _wait_time != 0.0 || _wait_frames != 0\n"
  },
  {
    "path": "addons/gut/awaiter.gd.uid",
    "content": "uid://cub0o1h1sbesy\n"
  },
  {
    "path": "addons/gut/cli/gut_cli.gd",
    "content": "extends Node\n\nvar Optparse = load('res://addons/gut/cli/optparse.gd')\nvar Gut = load('res://addons/gut/gut.gd')\nvar GutRunner = load('res://addons/gut/gui/GutRunner.tscn')\n\n# ------------------------------------------------------------------------------\n# Helper class to resolve the various different places where an option can\n# be set.  Using the get_value method will enforce the order of precedence of:\n# \t1.  command line value\n#\t2.  config file value\n#\t3.  default value\n#\n# The idea is that you set the base_opts.  That will get you a copies of the\n# hash with null values for the other types of values.  Lower precedented hashes\n# will punch through null values of higher precedented hashes.\n# ------------------------------------------------------------------------------\nclass OptionResolver:\n\tvar base_opts = {}\n\tvar cmd_opts = {}\n\tvar config_opts = {}\n\n\n\tfunc get_value(key):\n\t\treturn _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key]))\n\n\tfunc set_base_opts(opts):\n\t\tbase_opts = opts\n\t\tcmd_opts = _null_copy(opts)\n\t\tconfig_opts = _null_copy(opts)\n\n\t# creates a copy of a hash with all values null.\n\tfunc _null_copy(h):\n\t\tvar new_hash = {}\n\t\tfor key in h:\n\t\t\tnew_hash[key] = null\n\t\treturn new_hash\n\n\tfunc _nvl(a, b):\n\t\tif(a == null):\n\t\t\treturn b\n\t\telse:\n\t\t\treturn a\n\n\tfunc _string_it(h):\n\t\tvar to_return = ''\n\t\tfor key in h:\n\t\t\tto_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')')\n\t\treturn to_return\n\n\tfunc to_s():\n\t\treturn str(\"base:\\n\", _string_it(base_opts), \"\\n\", \\\n\t\t\t\t\"config:\\n\", _string_it(config_opts), \"\\n\", \\\n\t\t\t\t\"cmd:\\n\", _string_it(cmd_opts), \"\\n\", \\\n\t\t\t\t\"resolved:\\n\", _string_it(get_resolved_values()))\n\n\tfunc get_resolved_values():\n\t\tvar to_return = {}\n\t\tfor key in base_opts:\n\t\t\tto_return[key] = get_value(key)\n\t\treturn to_return\n\n\tfunc to_s_verbose():\n\t\tvar to_return = ''\n\t\tvar resolved = get_resolved_values()\n\t\tfor key in base_opts:\n\t\t\tto_return += str(key, \"\\n\")\n\t\t\tto_return += str('  default: ', _nvl(base_opts[key], 'NULL'), \"\\n\")\n\t\t\tto_return += str('  config:  ', _nvl(config_opts[key], ' --'), \"\\n\")\n\t\t\tto_return += str('  cmd:     ', _nvl(cmd_opts[key], ' --'), \"\\n\")\n\t\t\tto_return += str('  final:   ', _nvl(resolved[key], 'NULL'), \"\\n\")\n\n\t\treturn to_return\n\n# ------------------------------------------------------------------------------\n# Here starts the actual script that uses the Options class to kick off Gut\n# and run your tests.\n# ------------------------------------------------------------------------------\nvar _gut_config = load('res://addons/gut/gut_config.gd').new()\n\n# array of command line options specified\nvar _final_opts = []\n\n\nfunc setup_options(options, font_names):\n\tvar opts = Optparse.new()\n\topts.banner =\\\n\"\"\"\nThe GUT CLI\n-----------\nThe default behavior for GUT is to load options from a res://.gutconfig.json if\nit exists.  Any options specified on the command line will take precedence over\noptions specified in the gutconfig file.  You can specify a different gutconfig\nfile with the -gconfig option.\n\nTo generate a .gutconfig.json file you can use -gprint_gutconfig_sample\nTo see the effective values of a CLI command and a gutconfig use -gpo\n\nValues for options can be supplied using:\n    option=value    # no space around \"=\"\n    option value    # a space between option and value w/o =\n\nOptions whose values are lists/arrays can be specified multiple times:\n\t-gdir=a,b\n\t-gdir c,d\n\t-gdir e\n\t# results in -gdir equaling [a, b, c, d, e]\n\"\"\"\n\topts.add_heading(\"Test Config:\")\n\topts.add('-gdir', options.dirs, 'List of directories to search for test scripts in.')\n\topts.add('-ginclude_subdirs', false, 'Flag to include all subdirectories specified with -gdir.')\n\topts.add('-gtest', [], 'List of full paths to test scripts to run.')\n\topts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir.  Default \"[default]\".')\n\topts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension.  Default \"[default]\".')\n\topts.add('-gconfig', 'res://.gutconfig.json', 'The config file to load options from.  The default is [default].  Use \"-gconfig=\" to not use a config file.')\n\topts.add('-gpre_run_script', '', 'pre-run hook script path')\n\topts.add('-gpost_run_script', '', 'post-run hook script path')\n\topts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail.  With this option set, that does not happen.')\n\topts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling.  Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY].  Default \"[default]\"')\n\n\topts.add_heading(\"Run Options:\")\n\topts.add('-gselect', '', 'All scripts that contain the specified string in their filename will be ran')\n\topts.add('-ginner_class', '', 'Only run inner classes that contain the specified string in their name.')\n\topts.add('-gunit_test_name', '', 'Any test that contains the specified text will be run, all others will be skipped.')\n\topts.add('-gexit', false, 'Exit after running tests.  If not specified you have to manually close the window.')\n\topts.add('-gexit_on_success', false, 'Only exit if zero tests fail.')\n\topts.add('-gignore_pause', false, 'Ignores any calls to pause_before_teardown.')\n\n\topts.add_heading(\"Display Settings:\")\n\topts.add('-glog', options.log_level, 'Log level [0-3].  Default [default]')\n\topts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts.  Default [default].')\n\topts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.')\n\topts.add('-gcompact_mode', false, 'The runner will be in compact mode.  This overrides -gmaximize.')\n\topts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.')\n\topts.add('-gdisable_colors', false, 'Disable command line colors.')\n\topts.add('-gfont_name', options.font_name, str('Valid values are:  ', font_names, '.  Default \"[default]\"'))\n\topts.add('-gfont_size', options.font_size, 'Font size, default \"[default]\"')\n\topts.add('-gbackground_color', options.background_color, 'Background color as an html color, default \"[default]\"')\n\topts.add('-gfont_color',options.font_color, 'Font color as an html color, default \"[default]\"')\n\topts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI.  default [default]')\n\n\topts.add_heading(\"Result Export:\")\n\topts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.')\n\topts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, default [default]')\n\n\topts.add_heading(\"Help:\")\n\topts.add('-gh', false, 'Print this help.  You did this to see this, so you probably understand.')\n\topts.add('-gpo', false, 'Print option values from all sources and the value used.')\n\topts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.')\n\n\treturn opts\n\n\n# Parses options, applying them to the _tester or setting values\n# in the options struct.\nfunc extract_command_line_options(from, to):\n\tto.config_file = from.get_value_or_null('-gconfig')\n\tto.dirs = from.get_value_or_null('-gdir')\n\tto.disable_colors =  from.get_value_or_null('-gdisable_colors')\n\tto.double_strategy = from.get_value_or_null('-gdouble_strategy')\n\tto.ignore_pause = from.get_value_or_null('-gignore_pause')\n\tto.include_subdirs = from.get_value_or_null('-ginclude_subdirs')\n\tto.inner_class = from.get_value_or_null('-ginner_class')\n\tto.log_level = from.get_value_or_null('-glog')\n\tto.opacity = from.get_value_or_null('-gopacity')\n\tto.post_run_script = from.get_value_or_null('-gpost_run_script')\n\tto.pre_run_script = from.get_value_or_null('-gpre_run_script')\n\tto.prefix = from.get_value_or_null('-gprefix')\n\tto.selected = from.get_value_or_null('-gselect')\n\tto.should_exit = from.get_value_or_null('-gexit')\n\tto.should_exit_on_success = from.get_value_or_null('-gexit_on_success')\n\tto.should_maximize = from.get_value_or_null('-gmaximize')\n\tto.compact_mode = from.get_value_or_null('-gcompact_mode')\n\tto.hide_orphans = from.get_value_or_null('-ghide_orphans')\n\tto.suffix = from.get_value_or_null('-gsuffix')\n\tto.errors_do_not_cause_failure = from.get_value_or_null('-gerrors_do_not_cause_failure')\n\tto.tests = from.get_value_or_null('-gtest')\n\tto.unit_test_name = from.get_value_or_null('-gunit_test_name')\n\n\tto.font_size = from.get_value_or_null('-gfont_size')\n\tto.font_name = from.get_value_or_null('-gfont_name')\n\tto.background_color = from.get_value_or_null('-gbackground_color')\n\tto.font_color = from.get_value_or_null('-gfont_color')\n\tto.paint_after = from.get_value_or_null('-gpaint_after')\n\n\tto.junit_xml_file = from.get_value_or_null('-gjunit_xml_file')\n\tto.junit_xml_timestamp = from.get_value_or_null('-gjunit_xml_timestamp')\n\n\n\nfunc _print_gutconfigs(values):\n\tvar header = \"\"\"Here is a sample of a full .gutconfig.json file.\nYou do not need to specify all values in your own file.  The values supplied in\nthis sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample\noption.   Option priority is:  command-line, .gutconfig, default).\"\"\"\n\tprint(\"\\n\", header.replace(\"\\n\", ' '), \"\\n\")\n\tvar resolved = values\n\n\t# remove_at some options that don't make sense to be in config\n\tresolved.erase(\"config_file\")\n\tresolved.erase(\"show_help\")\n\n\tprint(JSON.stringify(resolved, '  '))\n\n\tfor key in resolved:\n\t\tresolved[key] = null\n\n\tprint(\"\\n\\nAnd here's an empty config for you fill in what you want.\")\n\tprint(JSON.stringify(resolved, ' '))\n\n\nfunc _run_tests(opt_resolver):\n\t_final_opts = opt_resolver.get_resolved_values();\n\t_gut_config.options = _final_opts\n\n\tvar runner = GutRunner.instantiate()\n\trunner.ran_from_editor = false\n\trunner.set_gut_config(_gut_config)\n\tget_tree().root.add_child(runner)\n\n\trunner.run_tests()\n\n\n# parse options and run Gut\nfunc main():\n\tvar opt_resolver = OptionResolver.new()\n\topt_resolver.set_base_opts(_gut_config.default_options)\n\n\tvar cli_opts = setup_options(_gut_config.default_options, _gut_config.valid_fonts)\n\n\tcli_opts.parse()\n\tvar all_options_valid = cli_opts.unused.size() == 0\n\textract_command_line_options(cli_opts, opt_resolver.cmd_opts)\n\n\tvar config_path = opt_resolver.get_value('config_file')\n\tvar load_result = 1\n\t# Checking for an empty config path allows us to not use a config file via\n\t# the -gconfig_file option since using \"-gconfig_file=\" or -gconfig_file=''\"\n\t# will result in an empty string.\n\tif(config_path != ''):\n\t\tload_result = _gut_config.load_options_no_defaults(config_path)\n\n\t# SHORTCIRCUIT\n\tif(!all_options_valid):\n\t\tprint('Unknown arguments:  ', cli_opts.unused)\n\t\tget_tree().quit(1)\n\telif(load_result == -1):\n\t\tprint('Invalid gutconfig ', load_result)\n\t\tget_tree().quit(1)\n\telse:\n\t\topt_resolver.config_opts = _gut_config.options\n\n\t\tif(cli_opts.get_value('-gh')):\n\t\t\tprint(GutUtils.version_numbers.get_version_text())\n\t\t\tcli_opts.print_help()\n\t\t\tget_tree().quit(0)\n\t\telif(cli_opts.get_value('-gpo')):\n\t\t\tprint('All config options and where they are specified.  ' +\n\t\t\t\t'The \"final\" value shows which value will actually be used ' +\n\t\t\t\t'based on order of precedence (default < .gutconfig < cmd line).' + \"\\n\")\n\t\t\tprint(opt_resolver.to_s_verbose())\n\t\t\tget_tree().quit(0)\n\t\telif(cli_opts.get_value('-gprint_gutconfig_sample')):\n\t\t\t_print_gutconfigs(opt_resolver.get_resolved_values())\n\t\t\tget_tree().quit(0)\n\t\telse:\n\t\t\t_run_tests(opt_resolver)\n\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/cli/gut_cli.gd.uid",
    "content": "uid://bvyb8bvwwhi67\n"
  },
  {
    "path": "addons/gut/cli/optparse.gd",
    "content": "# ##############################################################################\n# Parses options from the command line, as one might expect.  It can also\n# generate help text that displays all the arguments your script accepts.\n#\n# This does alot, if you want to see it in action have a look at\n#\tscratch/optparse_example.gd\n#\n#\n# Godot Argument Lists\n# -------------------------\n# There are two sets of command line arguments that Godot populates:\n#\tOS.get_cmdline_args\n#\tOS.get_cmdline_user_args.\n#\n# OS.get_cmdline_args contains any arguments that are not used by the engine\n# itself.  This means options like --help and -d will never appear in this list\n# since these are used by the engine.  The one exception is the -s option which\n# is always included as the first entry and the script path as the second.\n# Optparse ignores these values for argument processing but can be accessed\n# with my_optparse.options.script_option.  This list does not contain any\n# arguments that appear in OS.get_cmdline_user_args.\n#\n# OS.get_cmdline_user_args contains any arguments that appear on the command\n# line AFTER \" -- \" or \" ++ \".  This list CAN contain options that the engine\n# would otherwise use, and are ignored completely by the engine.\n#\n# The parse method, by default, includes arguments from OS.get_cmdline_args and\n# OS.get_cmdline_user_args.  You can optionally pass one of these to the parse\n# method to limit which arguments are parsed.  You can also conjure up your own\n# array of arguments and pass that to parse.\n#\n# See Godot's documentation for get_cmdline_args and get_cmdline_user_args for\n# more information.\n#\n#\n# Adding Options\n# --------------\n# Use the following to add options to be parsed.  These methods return the\n# created Option instance.  See that class above for more info.  You can use\n# the returned instance to get values, or use get_value/get_value_or_null.\n#\tadd(\"--name\", \"default\", \"Description goes here\")\n#\tadd_required(\"--name\", \"default\", \"Description goes here\")\n# \tadd_positional(\"--name\", \"default\", \"Description goes here\")\n#\tadd_positional_required(\"--name\", \"default\", \"Description goes here\")\n#\n# get_value will return the value of the option or the default if it was not\n# set.  get_value_or_null will return the value of the option or null if it was\n# not set.\n#\n# The Datatype for an option is determined from the default value supplied to\n# the various add methods.  Supported types are\n#\tString\n#\tInt\n#\tFloat\n#\tArray of strings\n#\tBoolean\n#\n#\n# Value Parsing\n# -------------\n# optparse uses option_name_prefix to differentiate between option names and\n# values.  Any argument that starts with this value will be treated as an\n# argument name.  The default is \"-\".  Set this before calling parse if you want\n# to change it.\n#\n# Values for options can be supplied on the command line with or without an \"=\":\n#\toption=value    # no space around \"=\"\n#\toption value    # a space between option and value w/o =\n# There is no way to escape \"=\" at this time.\n#\n# Array options can be specified multiple times and/or set from a comma delimited\n# list.\n# \t-gdir=a,b\n# \t-gdir c,d\n# \t-gdir e\n# Results in -gdir equaling [a, b, c, d, e].  There is no way to escape commas\n# at this time.\n#\n# To specify an empty list via the command line follow the option with an equal\n# sign\n#\t-gdir=\n#\n# Boolean options will have thier value set to !default when they are supplied\n# on the command line.  Boolean options cannot have a value on the command line.\n# They are either supplied or not.\n#\n# If a value is not an array and is specified multiple times on the command line\n# then the last entry will be used as the value.\n#\n# Positional argument values are parsed after all named arguments are parsed.\n# This means that other options can appear before, between, and after positional\n# arguments.\n#\t--foo=bar positional_0_value --disabled --bar foo positional_1_value --a_flag\n#\n# Anything that is not used by named or positional arguments will appear in the\n# unused property.  You can use this to detect unrecognized arguments or treat\n# everything else provided as a list of things, or whatever you want.  You can\n# use is_option on the elements of unused (or whatever you want really) to see\n# if optparse would treat it as an option name.\n#\n# Use get_missing_required_options to get an array of Option with all required\n# options that were not found when parsing.\n#\n# The parsed_args property holds the list of arguments that were parsed.\n#\n#\n# Help Generation\n# ---------------\n# You can call get_help to generate help text, or you can just call print_help\n# and this will print it for you.\n#\n# Set the banner property to any text you want to appear before the usage and\n# options sections.\n#\n# Options are printed in the order they are added.  You can add a heading for\n# different options sections with add_heading.\n# \tadd(\"--asdf\", 1, \"This will have no heading\")\n#\tadd_heading(\"foo\")\n#\tadd(\"--foo\", false, \"This will have the foo heading\")\n#\tadd(\"--another_foo\", 1.5, \"This too.\")\n#\tadd_heading(\"This is after foo\")\n#\tadd(\"--bar\", true, \"You probably get it by now.\")\n#\n# If you include \"[default]\" in the description of a option, then the help will\n# substitue it with the default value.\n#\n# ##############################################################################\n\n\n#-------------------------------------------------------------------------------\n# Holds all the properties of a command line option\n#\n# value will return the default when it has not been set.\n#-------------------------------------------------------------------------------\nclass Option:\n\tvar _has_been_set = false\n\tvar _value = null\n\t# REMEMBER that when this option is an array, you have to set the value\n\t# before you alter the contents of the array (append etc) or has_been_set\n\t# will return false and it might not be used right.  For example\n\t# get_value_or_null will return null when you've actually changed the value.\n\tvar value = _value:\n\t\tget:\n\t\t\treturn _value\n\n\t\tset(val):\n\t\t\t_has_been_set = true\n\t\t\t_value = val\n\n\tvar option_name = ''\n\tvar default = null\n\tvar description = ''\n\tvar required = false\n\n\n\tfunc _init(name,default_value,desc=''):\n\t\toption_name = name\n\t\tdefault = default_value\n\t\tdescription = desc\n\t\t_value = default\n\n\n\tfunc to_s(min_space=0):\n\t\tvar subbed_desc = description\n\t\tsubbed_desc = subbed_desc.replace('[default]', str(default))\n\t\treturn str(option_name.rpad(min_space), ' ', subbed_desc)\n\n\n\tfunc has_been_set():\n\t\treturn _has_been_set\n\n\n\n\n#-------------------------------------------------------------------------------\n# A struct for organizing options by a heading\n#-------------------------------------------------------------------------------\nclass OptionHeading:\n\tvar options = []\n\tvar display = 'default'\n\n\n\n\n#-------------------------------------------------------------------------------\n# Organizes options by order, heading, position.  Also responsible for all\n# help related text generation.\n#-------------------------------------------------------------------------------\nclass Options:\n\tvar options = []\n\tvar positional = []\n\tvar default_heading = OptionHeading.new()\n\tvar script_option = Option.new('-s', '?', 'script option provided by Godot')\n\n\tvar _options_by_name = {}\n\tvar _options_by_heading = [default_heading]\n\tvar _cur_heading = default_heading\n\n\n\tfunc add_heading(display):\n\t\tvar heading = OptionHeading.new()\n\t\theading.display = display\n\t\t_cur_heading = heading\n\t\t_options_by_heading.append(heading)\n\n\n\tfunc add(option):\n\t\toptions.append(option)\n\t\t_options_by_name[option.option_name] = option\n\t\t_cur_heading.options.append(option)\n\n\n\tfunc add_positional(option):\n\t\tpositional.append(option)\n\t\t_options_by_name[option.option_name] = option\n\n\n\tfunc get_by_name(option_name):\n\t\tvar found_param = null\n\t\tif(option_name == script_option.option_name):\n\t\t\tfound_param = script_option\n\t\telif(_options_by_name.has(option_name)):\n\t\t\tfound_param = _options_by_name[option_name]\n\n\t\treturn found_param\n\n\n\tfunc get_help_text():\n\t\tvar longest = 0\n\t\tvar text = \"\"\n\t\tfor i in range(options.size()):\n\t\t\tif(options[i].option_name.length() > longest):\n\t\t\t\tlongest = options[i].option_name.length()\n\n\t\tfor heading in _options_by_heading:\n\t\t\tif(heading != default_heading):\n\t\t\t\ttext += str(\"\\n\", heading.display, \"\\n\")\n\t\t\tfor option in heading.options:\n\t\t\t\ttext += str('  ', option.to_s(longest + 2), \"\\n\")\n\n\n\t\treturn text\n\n\n\tfunc get_option_value_text():\n\t\tvar text = \"\"\n\t\tvar i = 0\n\t\tfor option in positional:\n\t\t\ttext += str(i, '.  ', option.option_name, ' = ', option.value)\n\n\t\t\tif(!option.has_been_set()):\n\t\t\t\ttext += \" (default)\"\n\t\t\ttext += \"\\n\"\n\t\t\ti += 1\n\n\t\tfor option in options:\n\t\t\ttext += str(option.option_name, ' = ', option.value)\n\n\t\t\tif(!option.has_been_set()):\n\t\t\t\ttext += \" (default)\"\n\t\t\ttext += \"\\n\"\n\t\treturn text\n\n\n\tfunc print_option_values():\n\t\tprint(get_option_value_text())\n\n\n\tfunc get_missing_required_options():\n\t\tvar to_return = []\n\t\tfor opt in options:\n\t\t\tif(opt.required and !opt.has_been_set()):\n\t\t\t\tto_return.append(opt)\n\n\t\tfor opt in positional:\n\t\t\tif(opt.required and !opt.has_been_set()):\n\t\t\t\tto_return.append(opt)\n\n\t\treturn to_return\n\n\n\tfunc get_usage_text():\n\t\tvar pos_text = \"\"\n\t\tfor opt in positional:\n\t\t\tpos_text += str(\"[\", opt.description, \"] \")\n\n\t\tif(pos_text != \"\"):\n\t\t\tpos_text += \" [opts] \"\n\n\t\treturn \"<path to godot> -s \" + script_option.value + \" [opts] \" + pos_text\n\n\n\n\n#-------------------------------------------------------------------------------\n#\n# optarse\n#\n#-------------------------------------------------------------------------------\nvar options = Options.new()\nvar banner = ''\nvar option_name_prefix = '-'\nvar unused = []\nvar parsed_args = []\n\nfunc _convert_value_to_array(raw_value):\n\tvar split = raw_value.split(',')\n\t# This is what an empty set looks like from the command line.  If we do\n\t# not do this then we will always get back [''] which is not what it\n\t# shoudl be.\n\tif(split.size() == 1 and split[0] == ''):\n\t\tsplit = []\n\treturn split\n\n\n# REMEMBER raw_value not used for bools.\nfunc _set_option_value(option, raw_value):\n\tvar t = typeof(option.default)\n\t# only set values that were specified at the command line so that\n\t# we can punch through default and config values correctly later.\n\t# Without this check, you can't tell the difference between the\n\t# defaults and what was specified, so you can't punch through\n\t# higher level options.\n\tif(t == TYPE_INT):\n\t\toption.value = int(raw_value)\n\telif(t == TYPE_STRING):\n\t\toption.value = str(raw_value)\n\telif(t == TYPE_ARRAY):\n\t\tvar values = _convert_value_to_array(raw_value)\n\t\tif(!option.has_been_set()):\n\t\t\toption.value = []\n\t\toption.value.append_array(values)\n\telif(t == TYPE_BOOL):\n\t\toption.value = !option.default\n\telif(t == TYPE_FLOAT):\n\t\toption.value = float(raw_value)\n\telif(t == TYPE_NIL):\n\t\tprint(option.option_name + ' cannot be processed, it has a nil datatype')\n\telse:\n\t\tprint(option.option_name + ' cannot be processed, it has unknown datatype:' + str(t))\n\n\nfunc _parse_command_line_arguments(args):\n\tvar parsed_opts = args.duplicate()\n\tvar i = 0\n\tvar positional_index = 0\n\n\twhile i < parsed_opts.size():\n\t\tvar opt  = ''\n\t\tvar value = ''\n\t\tvar entry = parsed_opts[i]\n\n\t\tif(is_option(entry)):\n\t\t\tif(entry.find('=') != -1):\n\t\t\t\tvar parts = entry.split('=')\n\t\t\t\topt = parts[0]\n\t\t\t\tvalue = parts[1]\n\t\t\t\tvar the_option = options.get_by_name(opt)\n\t\t\t\tif(the_option != null):\n\t\t\t\t\tparsed_opts.remove_at(i)\n\t\t\t\t\t_set_option_value(the_option, value)\n\t\t\t\telse:\n\t\t\t\t\ti += 1\n\t\t\telse:\n\t\t\t\tvar the_option = options.get_by_name(entry)\n\t\t\t\tif(the_option != null):\n\t\t\t\t\tparsed_opts.remove_at(i)\n\t\t\t\t\tif(typeof(the_option.default) == TYPE_BOOL):\n\t\t\t\t\t\t_set_option_value(the_option, null)\n\t\t\t\t\telif(i < parsed_opts.size() and !is_option(parsed_opts[i])):\n\t\t\t\t\t\tvalue = parsed_opts[i]\n\t\t\t\t\t\tparsed_opts.remove_at(i)\n\t\t\t\t\t\t_set_option_value(the_option, value)\n\t\t\t\telse:\n\t\t\t\t\ti += 1\n\t\telse:\n\t\t\tif(positional_index < options.positional.size()):\n\t\t\t\t_set_option_value(options.positional[positional_index], entry)\n\t\t\t\tparsed_opts.remove_at(i)\n\t\t\t\tpositional_index += 1\n\t\t\telse:\n\t\t\t\ti += 1\n\n\t# this is the leftovers that were not extracted.\n\treturn parsed_opts\n\n\nfunc is_option(arg):\n\treturn arg.begins_with(option_name_prefix)\n\n\nfunc add(op_name, default, desc):\n\tvar new_op = null\n\n\tif(options.get_by_name(op_name) != null):\n\t\tpush_error(str('Option [', op_name, '] already exists.'))\n\telse:\n\t\tnew_op = Option.new(op_name, default, desc)\n\t\toptions.add(new_op)\n\n\treturn new_op\n\n\nfunc add_required(op_name, default, desc):\n\tvar op = add(op_name, default, desc)\n\tif(op != null):\n\t\top.required = true\n\treturn op\n\n\nfunc add_positional(op_name, default, desc):\n\tvar new_op = null\n\tif(options.get_by_name(op_name) != null):\n\t\tpush_error(str('Positional option [', op_name, '] already exists.'))\n\telse:\n\t\tnew_op = Option.new(op_name, default, desc)\n\t\toptions.add_positional(new_op)\n\treturn new_op\n\n\nfunc add_positional_required(op_name, default, desc):\n\tvar op = add_positional(op_name, default, desc)\n\tif(op != null):\n\t\top.required = true\n\treturn op\n\n\nfunc add_heading(display_text):\n\toptions.add_heading(display_text)\n\n\nfunc get_value(name):\n\tvar found_param = options.get_by_name(name)\n\n\tif(found_param != null):\n\t\treturn found_param.value\n\telse:\n\t\tprint(\"COULD NOT FIND OPTION \" + name)\n\t\treturn null\n\n\n# This will return null instead of the default value if an option has not been\n# specified.  This can be useful when providing an order of precedence to your\n# values.  For example if\n#\tdefault value < config file < command line\n# then you do not want to get the default value for a command line option or it\n# will overwrite the value in a config file.\nfunc get_value_or_null(name):\n\tvar found_param = options.get_by_name(name)\n\n\tif(found_param != null and found_param.has_been_set()):\n\t\treturn found_param.value\n\telse:\n\t\treturn null\n\n\nfunc get_help():\n\tvar sep = '---------------------------------------------------------'\n\n\tvar text = str(sep, \"\\n\", banner, \"\\n\\n\")\n\ttext += \"Usage\\n-----------\\n\"\n\ttext += \"  \" + options.get_usage_text() + \"\\n\\n\"\n\ttext += \"\\nOptions\\n-----------\\n\"\n\ttext += options.get_help_text()\n\ttext += str(sep, \"\\n\")\n\treturn text\n\n\nfunc print_help():\n\tprint(get_help())\n\n\nfunc parse(cli_args=null):\n\tparsed_args = cli_args\n\n\tif(parsed_args == null):\n\t\tparsed_args = OS.get_cmdline_args()\n\t\tparsed_args.append_array(OS.get_cmdline_user_args())\n\n\tunused = _parse_command_line_arguments(parsed_args)\n\n\nfunc get_missing_required_options():\n\treturn options.get_missing_required_options()\n\n\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2024 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################"
  },
  {
    "path": "addons/gut/cli/optparse.gd.uid",
    "content": "uid://c3h6h5q43socr\n"
  },
  {
    "path": "addons/gut/collected_script.gd",
    "content": "# ------------------------------------------------------------------------------\n# This holds all the meta information for a test script.  It contains the\n# name of the inner class and an array of CollectedTests.  This does not parse\n# anything, it just holds the data about parsed scripts and tests.  The\n# TestCollector is responsible for populating this object.\n#\n# This class also facilitates all the exporting and importing of tests.\n# ------------------------------------------------------------------------------\nvar CollectedTest = GutUtils.CollectedTest\n\nvar _lgr = null\n\n# One entry per test found in the script.  Added externally by TestCollector\nvar tests = []\n# One entry for before_all and after_all (maybe add before_each and after_each).\n# These are added by Gut when running before_all and after_all for the script.\nvar setup_teardown_tests = []\nvar inner_class_name:StringName\nvar path:String\n\n\n# Set externally by test_collector after it can verify that the script was\n# actually loaded.  This could probably be changed to just hold the GutTest\n# script that was loaded, cutting down on complexity elsewhere.\nvar is_loaded = false\n\n# Set by Gut when it decides that a script should be skipped.\n# Right now this is whenever the script has the variable skip_script declared.\n# the value of skip_script is put into skip_reason.\nvar was_skipped = false\nvar skip_reason = ''\nvar was_run = false\n\n\nvar name = '' :\n\tget: return path\n\tset(val):pass\n\n\nfunc _init(logger=null):\n\t_lgr = logger\n\n\nfunc get_new():\n\treturn load_script().new()\n\n\nfunc load_script():\n\tvar to_return = load(path)\n\n\tif(inner_class_name != null and inner_class_name != ''):\n\t\t# If we wanted to do inner classes in inner classses\n\t\t# then this would have to become some kind of loop or recursive\n\t\t# call to go all the way down the chain or this class would\n\t\t# have to change to hold onto the loaded class instead of\n\t\t# just path information.\n\t\tto_return = to_return.get(inner_class_name)\n\n\treturn to_return\n\n# script.gd.InnerClass\nfunc get_filename_and_inner():\n\tvar to_return = get_filename()\n\tif(inner_class_name != ''):\n\t\tto_return += '.' + String(inner_class_name)\n\treturn to_return\n\n\n# res://foo/bar.gd.FooBar\nfunc get_full_name():\n\tvar to_return = path\n\tif(inner_class_name != ''):\n\t\tto_return += '.' + String(inner_class_name)\n\treturn to_return\n\n\nfunc get_filename():\n\treturn path.get_file()\n\n\nfunc has_inner_class():\n\treturn inner_class_name != ''\n\n\n# Note:  although this no longer needs to export the inner_class names since\n#        they are pulled from metadata now, it is easier to leave that in\n#        so we don't have to cut the export down to unique script names.\nfunc export_to(config_file, section):\n\tconfig_file.set_value(section, 'path', path)\n\tconfig_file.set_value(section, 'inner_class', inner_class_name)\n\tvar names = []\n\tfor i in range(tests.size()):\n\t\tnames.append(tests[i].name)\n\tconfig_file.set_value(section, 'tests', names)\n\n\nfunc _remap_path(source_path):\n\tvar to_return = source_path\n\tif(!FileAccess.file_exists(source_path)):\n\t\t_lgr.debug('Checking for remap for:  ' + source_path)\n\t\tvar remap_path = source_path.get_basename() + '.gd.remap'\n\t\tif(FileAccess.file_exists(remap_path)):\n\t\t\tvar cf = ConfigFile.new()\n\t\t\tcf.load(remap_path)\n\t\t\tto_return = cf.get_value('remap', 'path')\n\t\telse:\n\t\t\t_lgr.warn('Could not find remap file ' + remap_path)\n\treturn to_return\n\n\nfunc import_from(config_file, section):\n\tpath = config_file.get_value(section, 'path')\n\tpath = _remap_path(path)\n\t# Null is an acceptable value, but you can't pass null as a default to\n\t# get_value since it thinks you didn't send a default...then it spits\n\t# out red text.  This works around that.\n\tvar inner_name = config_file.get_value(section, 'inner_class', 'Placeholder')\n\tif(inner_name != 'Placeholder'):\n\t\tinner_class_name = inner_name\n\telse: # just being explicit\n\t\tinner_class_name = StringName(\"\")\n\n\nfunc get_test_named(test_name):\n\treturn GutUtils.search_array(tests, 'name', test_name)\n\n\nfunc get_ran_test_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tif(t.was_run):\n\t\t\tcount += 1\n\treturn count\n\n\nfunc get_assert_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tcount += t.pass_texts.size()\n\t\tcount += t.fail_texts.size()\n\tfor t in setup_teardown_tests:\n\t\tcount += t.pass_texts.size()\n\t\tcount += t.fail_texts.size()\n\treturn count\n\n\nfunc get_pass_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tcount += t.pass_texts.size()\n\tfor t in setup_teardown_tests:\n\t\tcount += t.pass_texts.size()\n\treturn count\n\n\nfunc get_fail_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tcount += t.fail_texts.size()\n\tfor t in setup_teardown_tests:\n\t\tcount += t.fail_texts.size()\n\treturn count\n\n\nfunc get_pending_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tcount += t.pending_texts.size()\n\treturn count\n\n\nfunc get_passing_test_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tif(t.is_passing()):\n\t\t\tcount += 1\n\treturn count\n\n\nfunc get_failing_test_count():\n\tvar count = 0\n\tfor t in tests:\n\t\tif(t.is_failing()):\n\t\t\tcount += 1\n\treturn count\n\n\nfunc get_risky_count():\n\tvar count = 0\n\tif(was_skipped):\n\t\tcount = 1\n\telse:\n\t\tfor t in tests:\n\t\t\tif(t.is_risky()):\n\t\t\t\tcount += 1\n\treturn count\n\n\nfunc to_s():\n\tvar to_return = path\n\tif(inner_class_name != null):\n\t\tto_return += str('.', inner_class_name)\n\tto_return += \"\\n\"\n\tfor i in range(tests.size()):\n\t\tto_return += str('  ', tests[i].to_s())\n\treturn to_return\n"
  },
  {
    "path": "addons/gut/collected_script.gd.uid",
    "content": "uid://dsl71gqb5syn4\n"
  },
  {
    "path": "addons/gut/collected_test.gd",
    "content": "# ------------------------------------------------------------------------------\n# Used to keep track of info about each test ran.\n# ------------------------------------------------------------------------------\n# the name of the function\nvar name = \"\"\n\n# flag to know if the name has been printed yet.  Used by the logger.\nvar has_printed_name = false\n\n# the number of arguments the method has\nvar arg_count = 0\n\n# the time it took to execute the test in seconds\nvar time_taken : float = 0\n\n# The number of asserts in the test.  Converted to a property for backwards\n# compatibility.  This now reflects the text sizes instead of being a value\n# that can be altered externally.\nvar assert_count = 0 :\n\tget: return pass_texts.size() + fail_texts.size()\n\tset(val): pass\n\n# Converted to propety for backwards compatibility.  This now cannot be set\n# externally\nvar pending = false :\n\tget: return is_pending()\n\tset(val): pass\n\n# the line number when the test fails\nvar line_number = -1\n\n# Set internally by Gut using whatever reason Gut wants to use to set this.\n# Gut will skip these marked true and the test will be listed as risky.\nvar should_skip = false  # -- Currently not used by GUT don't believe ^\n\nvar pass_texts = []\nvar fail_texts = []\nvar pending_texts = []\nvar orphans = 0\n\nvar was_run = false\n\n\nfunc did_pass():\n\treturn is_passing()\n\n\nfunc add_fail(fail_text):\n\tfail_texts.append(fail_text)\n\n\nfunc add_pending(pending_text):\n\tpending_texts.append(pending_text)\n\n\nfunc add_pass(passing_text):\n\tpass_texts.append(passing_text)\n\n\n# must have passed an assert and not have any other status to be passing\nfunc is_passing():\n\treturn pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0\n\n\n# failing takes precedence over everything else, so any failures makes the\n# test a failure.\nfunc is_failing():\n\treturn fail_texts.size() > 0\n\n\n# test is only pending if pending was called and the test is not failing.\nfunc is_pending():\n\treturn pending_texts.size() > 0 and fail_texts.size() == 0\n\n\nfunc is_risky():\n\treturn should_skip or (was_run and !did_something())\n\n\nfunc did_something():\n\treturn is_passing() or is_failing() or is_pending()\n\n\nfunc get_status_text():\n\tvar to_return = GutUtils.TEST_STATUSES.NO_ASSERTS\n\n\tif(should_skip):\n\t\tto_return = GutUtils.TEST_STATUSES.SKIPPED\n\telif(!was_run):\n\t\tto_return = GutUtils.TEST_STATUSES.NOT_RUN\n\telif(pending_texts.size() > 0):\n\t\tto_return = GutUtils.TEST_STATUSES.PENDING\n\telif(fail_texts.size() > 0):\n\t\tto_return = GutUtils.TEST_STATUSES.FAILED\n\telif(pass_texts.size() > 0):\n\t\tto_return = GutUtils.TEST_STATUSES.PASSED\n\n\treturn to_return\n\n\n# Deprecated\nfunc get_status():\n\treturn get_status_text()\n\n\nfunc to_s():\n\tvar pad = '     '\n\tvar to_return = str(name, \"[\", get_status_text(), \"]\\n\")\n\n\tfor i in range(fail_texts.size()):\n\t\tto_return += str(pad, 'Fail:  ', fail_texts[i])\n\tfor i in range(pending_texts.size()):\n\t\tto_return += str(pad, 'Pending:  ', pending_texts[i], \"\\n\")\n\tfor i in range(pass_texts.size()):\n\t\tto_return += str(pad, 'Pass:  ', pass_texts[i], \"\\n\")\n\treturn to_return\n\n\n"
  },
  {
    "path": "addons/gut/collected_test.gd.uid",
    "content": "uid://b2edfqjiogifb\n"
  },
  {
    "path": "addons/gut/comparator.gd",
    "content": "var _strutils = GutUtils.Strutils.new()\nvar _max_length = 100\nvar _should_compare_int_to_float = true\n\nconst MISSING = '|__missing__gut__compare__value__|'\n\n\nfunc _cannot_compare_text(v1, v2):\n\treturn str('Cannot compare ', _strutils.types[typeof(v1)], ' with ',\n\t\t_strutils.types[typeof(v2)], '.')\n\n\nfunc _make_missing_string(text):\n\treturn '<missing ' + text + '>'\n\n\nfunc _create_missing_result(v1, v2, text):\n\tvar to_return = null\n\tvar v1_str = format_value(v1)\n\tvar v2_str = format_value(v2)\n\n\tif(typeof(v1) == TYPE_STRING and v1 == MISSING):\n\t\tv1_str = _make_missing_string(text)\n\t\tto_return = GutUtils.CompareResult.new()\n\telif(typeof(v2) == TYPE_STRING and v2 == MISSING):\n\t\tv2_str = _make_missing_string(text)\n\t\tto_return = GutUtils.CompareResult.new()\n\n\tif(to_return != null):\n\t\tto_return.summary = str(v1_str, ' != ', v2_str)\n\t\tto_return.are_equal = false\n\n\treturn to_return\n\n\nfunc simple(v1, v2, missing_string=''):\n\tvar missing_result = _create_missing_result(v1, v2, missing_string)\n\tif(missing_result != null):\n\t\treturn missing_result\n\n\tvar result = GutUtils.CompareResult.new()\n\tvar cmp_str = null\n\tvar extra = ''\n\n\tvar tv1 = typeof(v1)\n\tvar tv2 = typeof(v2)\n\n\t# print(tv1, '::', tv2, '   ', _strutils.types[tv1], '::', _strutils.types[tv2])\n\tif(_should_compare_int_to_float and [TYPE_INT, TYPE_FLOAT].has(tv1) and [TYPE_INT, TYPE_FLOAT].has(tv2)):\n\t\tresult.are_equal = v1 == v2\n\telif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)):\n\t\tresult.are_equal = v1 == v2\n\telif(GutUtils.are_datatypes_same(v1, v2)):\n\t\tresult.are_equal = v1 == v2\n\n\t\tif(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY):\n\t\t\tvar sub_result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)\n\t\t\tresult.summary = sub_result.get_short_summary()\n\t\t\tif(!sub_result.are_equal):\n\t\t\t\textra = \".\\n\" + sub_result.get_short_summary()\n\telse:\n\t\tcmp_str = '!='\n\t\tresult.are_equal = false\n\t\textra = str('.  ', _cannot_compare_text(v1, v2))\n\n\tcmp_str = get_compare_symbol(result.are_equal)\n\tresult.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra)\n\n\treturn result\n\n\nfunc shallow(v1, v2):\n\tvar result =  null\n\tif(GutUtils.are_datatypes_same(v1, v2)):\n\t\tif(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):\n\t\t\tresult = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)\n\t\telse:\n\t\t\tresult = simple(v1, v2)\n\telse:\n\t\tresult = simple(v1, v2)\n\n\treturn result\n\n\nfunc deep(v1, v2):\n\tvar result =  null\n\n\tif(GutUtils.are_datatypes_same(v1, v2)):\n\t\tif(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):\n\t\t\tresult = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)\n\t\telse:\n\t\t\tresult = simple(v1, v2)\n\telse:\n\t\tresult = simple(v1, v2)\n\n\treturn result\n\n\nfunc format_value(val, max_val_length=_max_length):\n\treturn _strutils.truncate_string(_strutils.type2str(val), max_val_length)\n\n\nfunc compare(v1, v2, diff_type=GutUtils.DIFF.SIMPLE):\n\tvar result = null\n\tif(diff_type == GutUtils.DIFF.SIMPLE):\n\t\tresult = simple(v1, v2)\n\telif(diff_type ==  GutUtils.DIFF.DEEP):\n\t\tresult = deep(v1, v2)\n\n\treturn result\n\n\nfunc get_should_compare_int_to_float():\n\treturn _should_compare_int_to_float\n\n\nfunc set_should_compare_int_to_float(should_compare_int_float):\n\t_should_compare_int_to_float = should_compare_int_float\n\n\nfunc get_compare_symbol(is_equal):\n\tif(is_equal):\n\t\treturn '=='\n\telse:\n\t\treturn '!='\n"
  },
  {
    "path": "addons/gut/comparator.gd.uid",
    "content": "uid://3psq3ss4wepq\n"
  },
  {
    "path": "addons/gut/compare_result.gd",
    "content": "var _are_equal = false\nvar are_equal = false :\n\tget:\n\t\treturn get_are_equal()\n\tset(val):\n\t\tset_are_equal(val)\n\nvar _summary = null\nvar summary = null :\n\tget:\n\t\treturn get_summary()\n\tset(val):\n\t\tset_summary(val)\n\nvar _max_differences = 30\nvar max_differences = 30 :\n\tget:\n\t\treturn get_max_differences()\n\tset(val):\n\t\tset_max_differences(val)\n\nvar _differences = {}\nvar differences :\n\tget:\n\t\treturn get_differences()\n\tset(val):\n\t\tset_differences(val)\n\nfunc _block_set(which, val):\n\tpush_error(str('cannot set ', which, ', value [', val, '] ignored.'))\n\nfunc _to_string():\n\treturn str(get_summary()) # could be null, gotta str it.\n\nfunc get_are_equal():\n\treturn _are_equal\n\nfunc set_are_equal(r_eq):\n\t_are_equal = r_eq\n\nfunc get_summary():\n\treturn _summary\n\nfunc set_summary(smry):\n\t_summary = smry\n\nfunc get_total_count():\n\tpass\n\nfunc get_different_count():\n\tpass\n\nfunc get_short_summary():\n\treturn summary\n\nfunc get_max_differences():\n\treturn _max_differences\n\nfunc set_max_differences(max_diff):\n\t_max_differences = max_diff\n\nfunc get_differences():\n\treturn _differences\n\nfunc set_differences(diffs):\n\t_block_set('differences', diffs)\n\nfunc get_brackets():\n\treturn null\n\n"
  },
  {
    "path": "addons/gut/compare_result.gd.uid",
    "content": "uid://dscxpe8c6ytjo\n"
  },
  {
    "path": "addons/gut/diff_formatter.gd",
    "content": "var _strutils = GutUtils.Strutils.new()\nconst INDENT = '    '\nvar _max_to_display = 30\nconst ABSOLUTE_MAX_DISPLAYED = 10000\nconst UNLIMITED = -1\n\n\nfunc _single_diff(diff, depth=0):\n\tvar to_return = \"\"\n\tvar brackets = diff.get_brackets()\n\n\tif(brackets != null and !diff.are_equal):\n\t\tto_return = ''\n\t\tto_return += str(brackets.open, \"\\n\",\n\t\t\t_strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), \"\\n\",\n\t\t\tbrackets.close)\n\telse:\n\t\tto_return = str(diff)\n\n\treturn to_return\n\n\nfunc make_it(diff):\n\tvar to_return = ''\n\tif(diff.are_equal):\n\t\tto_return = diff.summary\n\telse:\n\t\tif(_max_to_display ==  ABSOLUTE_MAX_DISPLAYED):\n\t\t\tto_return = str(diff.get_value_1(), ' != ', diff.get_value_2())\n\t\telse:\n\t\t\tto_return = diff.get_short_summary()\n\t\tto_return +=  str(\"\\n\", _strutils.indent_text(_single_diff(diff, 0), 1, '  '))\n\treturn to_return\n\n\nfunc differences_to_s(differences, depth=0):\n\tvar to_return = ''\n\tvar keys = differences.keys()\n\tkeys.sort()\n\tvar limit = min(_max_to_display, differences.size())\n\n\tfor i in range(limit):\n\t\tvar key = keys[i]\n\t\tto_return += str(key, \":  \", _single_diff(differences[key], depth))\n\n\t\tif(i != limit -1):\n\t\t\tto_return += \"\\n\"\n\n\tif(differences.size() > _max_to_display):\n\t\tto_return += str(\"\\n\\n... \", differences.size() - _max_to_display, \" more.\")\n\n\treturn to_return\n\n\nfunc get_max_to_display():\n\treturn _max_to_display\n\n\nfunc set_max_to_display(max_to_display):\n\t_max_to_display = max_to_display\n\tif(_max_to_display == UNLIMITED):\n\t\t_max_to_display = ABSOLUTE_MAX_DISPLAYED\n\n"
  },
  {
    "path": "addons/gut/diff_formatter.gd.uid",
    "content": "uid://be6htaq8rkbd0\n"
  },
  {
    "path": "addons/gut/diff_tool.gd",
    "content": "extends 'res://addons/gut/compare_result.gd'\nconst INDENT = '    '\nenum {\n\tDEEP,\n\tSIMPLE\n}\n\nvar _strutils = GutUtils.Strutils.new()\nvar _compare = GutUtils.Comparator.new()\nvar DiffTool = load('res://addons/gut/diff_tool.gd')\n\nvar _value_1 = null\nvar _value_2 = null\nvar _total_count = 0\nvar _diff_type = null\nvar _brackets = null\nvar _valid = true\nvar _desc_things = 'somethings'\n\n# -------- comapre_result.gd \"interface\" ---------------------\nfunc set_are_equal(val):\n\t_block_set('are_equal', val)\n\nfunc get_are_equal():\n\tif(!_valid):\n\t\treturn null\n\telse:\n\t\treturn differences.size() == 0\n\n\nfunc set_summary(val):\n\t_block_set('summary', val)\n\nfunc get_summary():\n\treturn summarize()\n\nfunc get_different_count():\n\treturn differences.size()\n\nfunc  get_total_count():\n\treturn _total_count\n\nfunc get_short_summary():\n\tvar text = str(_strutils.truncate_string(str(_value_1), 50),\n\t\t' ', _compare.get_compare_symbol(are_equal), ' ',\n\t\t_strutils.truncate_string(str(_value_2), 50))\n\tif(!are_equal):\n\t\ttext += str('  ', get_different_count(), ' of ', get_total_count(),\n\t\t\t' ', _desc_things, ' do not match.')\n\treturn text\n\nfunc get_brackets():\n\treturn _brackets\n# -------- comapre_result.gd \"interface\" ---------------------\n\n\nfunc _invalidate():\n\t_valid = false\n\tdifferences = null\n\n\nfunc _init(v1,v2,diff_type=DEEP):\n\t_value_1 = v1\n\t_value_2 = v2\n\t_diff_type = diff_type\n\t_compare.set_should_compare_int_to_float(false)\n\t_find_differences(_value_1, _value_2)\n\n\nfunc _find_differences(v1, v2):\n\tif(GutUtils.are_datatypes_same(v1, v2)):\n\t\tif(typeof(v1) == TYPE_ARRAY):\n\t\t\t_brackets = {'open':'[', 'close':']'}\n\t\t\t_desc_things = 'indexes'\n\t\t\t_diff_array(v1, v2)\n\t\telif(typeof(v2) == TYPE_DICTIONARY):\n\t\t\t_brackets = {'open':'{', 'close':'}'}\n\t\t\t_desc_things = 'keys'\n\t\t\t_diff_dictionary(v1, v2)\n\t\telse:\n\t\t\t_invalidate()\n\t\t\tGutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')\n\telse:\n\t\t_invalidate()\n\t\tGutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')\n\n\nfunc _diff_array(a1, a2):\n\t_total_count = max(a1.size(), a2.size())\n\tfor i in range(a1.size()):\n\t\tvar result = null\n\t\tif(i < a2.size()):\n\t\t\tif(_diff_type == DEEP):\n\t\t\t\tresult = _compare.deep(a1[i], a2[i])\n\t\t\telse:\n\t\t\t\tresult = _compare.simple(a1[i], a2[i])\n\t\telse:\n\t\t\tresult = _compare.simple(a1[i], _compare.MISSING, 'index')\n\n\t\tif(!result.are_equal):\n\t\t\tdifferences[i] = result\n\n\tif(a1.size() < a2.size()):\n\t\tfor i in range(a1.size(), a2.size()):\n\t\t\tdifferences[i] = _compare.simple(_compare.MISSING, a2[i], 'index')\n\n\nfunc _diff_dictionary(d1, d2):\n\tvar d1_keys = d1.keys()\n\tvar d2_keys = d2.keys()\n\n\t# Process all the keys in d1\n\t_total_count += d1_keys.size()\n\tfor key in d1_keys:\n\t\tif(!d2.has(key)):\n\t\t\tdifferences[key] = _compare.simple(d1[key], _compare.MISSING, 'key')\n\t\telse:\n\t\t\td2_keys.remove_at(d2_keys.find(key))\n\n\t\t\tvar result = null\n\t\t\tif(_diff_type == DEEP):\n\t\t\t\tresult = _compare.deep(d1[key], d2[key])\n\t\t\telse:\n\t\t\t\tresult = _compare.simple(d1[key], d2[key])\n\n\t\t\tif(!result.are_equal):\n\t\t\t\tdifferences[key] = result\n\n\t# Process all the keys in d2 that didn't exist in d1\n\t_total_count += d2_keys.size()\n\tfor i in range(d2_keys.size()):\n\t\tdifferences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key')\n\n\nfunc summarize():\n\tvar summary = ''\n\n\tif(are_equal):\n\t\tsummary = get_short_summary()\n\telse:\n\t\tvar formatter = load('res://addons/gut/diff_formatter.gd').new()\n\t\tformatter.set_max_to_display(max_differences)\n\t\tsummary = formatter.make_it(self)\n\n\treturn summary\n\n\nfunc get_diff_type():\n\treturn _diff_type\n\n\nfunc get_value_1():\n\treturn _value_1\n\n\nfunc get_value_2():\n\treturn _value_2\n"
  },
  {
    "path": "addons/gut/diff_tool.gd.uid",
    "content": "uid://c5u6dl5cwn26w\n"
  },
  {
    "path": "addons/gut/double_templates/function_template.txt",
    "content": "{func_decleration}\n\t{vararg_warning}__gutdbl.spy_on('{method_name}', {param_array})\n\tif(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})):\n\t\treturn {super_call}\n\telse:\n\t\treturn await __gutdbl.handle_other_stubs('{method_name}', {param_array})\n"
  },
  {
    "path": "addons/gut/double_templates/init_template.txt",
    "content": "{func_decleration}:\n\tsuper({super_params})\n\t__gutdbl.spy_on('{method_name}', {param_array})\n\n"
  },
  {
    "path": "addons/gut/double_templates/script_template.txt",
    "content": "# ##############################################################################\n# Gut Doubled Script\n# ##############################################################################\n{extends}\n\n{constants}\n\n{properties}\n\n# ------------------------------------------------------------------------------\n# GUT stuff\n# ------------------------------------------------------------------------------\nvar __gutdbl_values = {\n\tdouble = self,\n\tthepath = '{path}',\n\tsubpath = '{subpath}',\n\tstubber = {stubber_id},\n\tspy = {spy_id},\n\tgut = {gut_id},\n\tfrom_singleton = '{singleton_name}',\n\tis_partial = {is_partial},\n\tdoubled_methods = {doubled_methods},\n}\nvar __gutdbl = load('res://addons/gut/double_tools.gd').new(__gutdbl_values)\n\n# Here so other things can check for a method to know if this is a double.\nfunc __gutdbl_check_method__():\n\tpass\n\n# ------------------------------------------------------------------------------\n# Doubled Methods\n# ------------------------------------------------------------------------------\n"
  },
  {
    "path": "addons/gut/double_tools.gd",
    "content": "var thepath = ''\nvar subpath = ''\nvar stubber = null\nvar spy = null\nvar gut = null\nvar from_singleton = null\nvar is_partial = null\nvar double = null\n\nconst NO_DEFAULT_VALUE = '!__gut__no__default__value__!'\nfunc _init(values=null):\n\tif(values != null):\n\t\tdouble = values.double\n\t\tthepath = values.thepath\n\t\tsubpath = values.subpath\n\t\tstubber = from_id(values.stubber)\n\t\tspy = from_id(values.spy)\n\t\tgut = from_id(values.gut)\n\t\tfrom_singleton = values.from_singleton\n\t\tis_partial = values.is_partial\n\n\tif(gut != null):\n\t\tgut.get_autofree().add_free(double)\n\n\nfunc _get_stubbed_method_to_call(method_name, called_with):\n\tvar method = stubber.get_call_this(double, method_name, called_with)\n\tif(method != null):\n\t\tmethod = method.bindv(called_with)\n\t\treturn method\n\treturn method\n\n\nfunc from_id(inst_id):\n\tif(inst_id ==  -1):\n\t\treturn null\n\telse:\n\t\treturn instance_from_id(inst_id)\n\n\nfunc is_stubbed_to_call_super(method_name, called_with):\n\tif(stubber != null):\n\t\treturn stubber.should_call_super(double, method_name, called_with)\n\telse:\n\t\treturn false\n\n\nfunc handle_other_stubs(method_name, called_with):\n\tif(stubber == null):\n\t\treturn\n\n\tvar method = _get_stubbed_method_to_call(method_name, called_with)\n\tif(method != null):\n\t\treturn await method.call()\n\telse:\n\t\treturn stubber.get_return(double, method_name, called_with)\n\n\nfunc spy_on(method_name, called_with):\n\tif(spy != null):\n\t\tspy.add_call(double, method_name, called_with)\n\n\nfunc default_val(method_name, p_index, default_val=NO_DEFAULT_VALUE):\n\tif(stubber != null):\n\t\treturn stubber.get_default_value(double, method_name, p_index)\n\telse:\n\t\treturn null\n\n\nfunc vararg_warning():\n\tif(gut != null):\n\t\tgut.get_logger().warn(\n\t\t\t\"This method contains a vararg argument and the paramter count was not stubbed.  \" + \\\n\t\t\t\"GUT adds extra parameters to this method which should fill most needs.  \" + \\\n\t\t\t\"It is recommended that you stub param_count for this object's class to ensure \" + \\\n\t\t\t\"that there are not any parameter count mismatch errors.\")\n"
  },
  {
    "path": "addons/gut/double_tools.gd.uid",
    "content": "uid://b2qjwou7pm0tr\n"
  },
  {
    "path": "addons/gut/doubler.gd",
    "content": "# ------------------------------------------------------------------------------\n# A stroke of genius if I do say so.  This allows for doubling a scene without\n# having  to write any files.  By overloading the \"instantiate\" method  we can\n# make whatever we want.\n# ------------------------------------------------------------------------------\nclass PackedSceneDouble:\n\textends PackedScene\n\tvar _script =  null\n\tvar _scene = null\n\n\tfunc set_script_obj(obj):\n\t\t_script = obj\n\n\t@warning_ignore(\"native_method_override\")\n\tfunc instantiate(edit_state=0):\n\t\tvar inst = _scene.instantiate(edit_state)\n\t\tvar export_props = []\n\t\tvar script_export_flag = (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_SCRIPT_VARIABLE)\n\n\t\tif(_script !=  null):\n\t\t\tif(inst.get_script() != null):\n\t\t\t\t# Get all the exported props and values so we can set them again\n\t\t\t\tfor prop in inst.get_property_list():\n\t\t\t\t\tvar is_export = prop.usage & (script_export_flag) == script_export_flag\n\t\t\t\t\tif(is_export):\n\t\t\t\t\t\texport_props.append([prop.name, inst.get(prop.name)])\n\n\t\t\tinst.set_script(_script)\n\t\t\tfor exported_value in export_props:\n\t\t\t\tinst.set(exported_value[0], exported_value[1])\n\n\t\treturn inst\n\n\tfunc load_scene(path):\n\t\t_scene = load(path)\n\n\n\n\n# ------------------------------------------------------------------------------\n# START Doubler\n# ------------------------------------------------------------------------------\nvar _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')\nvar _script_collector = GutUtils.ScriptCollector.new()\n# used by tests for debugging purposes.\nvar print_source = false\nvar inner_class_registry = GutUtils.InnerClassRegistry.new()\n\n# ###############\n# Properties\n# ###############\nvar _stubber = GutUtils.Stubber.new()\nfunc get_stubber():\n\treturn _stubber\nfunc set_stubber(stubber):\n\t_stubber = stubber\n\nvar _lgr = GutUtils.get_logger()\nfunc get_logger():\n\treturn _lgr\nfunc set_logger(logger):\n\t_lgr = logger\n\t_method_maker.set_logger(logger)\n\nvar _spy = null\nfunc get_spy():\n\treturn _spy\nfunc set_spy(spy):\n\t_spy = spy\n\nvar _gut = null\nfunc get_gut():\n\treturn _gut\nfunc set_gut(gut):\n\t_gut = gut\n\nvar _strategy = null\nfunc get_strategy():\n\treturn _strategy\nfunc set_strategy(strategy):\n\tif(GutUtils.DOUBLE_STRATEGY.values().has(strategy)):\n\t\t_strategy = strategy\n\telse:\n\t\t_lgr.error(str('doubler.gd:  invalid double strategy ', strategy))\n\n\nvar _method_maker = GutUtils.MethodMaker.new()\nfunc get_method_maker():\n\treturn _method_maker\n\nvar _ignored_methods = GutUtils.OneToMany.new()\nfunc get_ignored_methods():\n\treturn _ignored_methods\n\n# ###############\n# Private\n# ###############\nfunc _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY):\n\tset_logger(GutUtils.get_logger())\n\t_strategy = strategy\n\n\nfunc _get_indented_line(indents, text):\n\tvar to_return = ''\n\tfor _i in range(indents):\n\t\tto_return += \"\\t\"\n\treturn str(to_return, text, \"\\n\")\n\n\nfunc _stub_to_call_super(parsed, method_name):\n\tif(!parsed.get_method(method_name).is_eligible_for_doubling()):\n\t\treturn\n\n\tvar params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath)\n\tparams.to_call_super()\n\t_stubber.add_stub(params)\n\n\nfunc _get_base_script_text(parsed, override_path, partial, included_methods):\n\tvar path = parsed.script_path\n\tif(override_path != null):\n\t\tpath = override_path\n\n\tvar stubber_id = -1\n\tif(_stubber != null):\n\t\tstubber_id = _stubber.get_instance_id()\n\n\tvar spy_id = -1\n\tif(_spy != null):\n\t\tspy_id = _spy.get_instance_id()\n\n\tvar gut_id = -1\n\tif(_gut != null):\n\t\tgut_id = _gut.get_instance_id()\n\n\tvar extends_text  = parsed.get_extends_text()\n\n\tvar values = {\n\t\t# Top  sections\n\t\t\"extends\":extends_text,\n\t\t\"constants\":'',#obj_info.get_constants_text(),\n\t\t\"properties\":'',#obj_info.get_properties_text(),\n\n\t\t# metadata values\n\t\t\"path\":path,\n\t\t\"subpath\":GutUtils.nvl(parsed.subpath, ''),\n\t\t\"stubber_id\":stubber_id,\n\t\t\"spy_id\":spy_id,\n\t\t\"gut_id\":gut_id,\n\t\t\"singleton_name\":'',#GutUtils.nvl(obj_info.get_singleton_name(), ''),\n\t\t\"is_partial\":partial,\n\t\t\"doubled_methods\":included_methods,\n\t}\n\n\treturn _base_script_text.format(values)\n\n\nfunc _is_method_eligible_for_doubling(parsed_script, parsed_method):\n\treturn !parsed_method.is_accessor() and \\\n\t\tparsed_method.is_eligible_for_doubling() and \\\n\t\t!_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)\n\n\n# Disable the native_method_override setting so that doubles do not generate\n# errors or warnings when doubling with INCLUDE_NATIVE or when a method has\n# been added because of param_count stub.\nfunc _create_script_no_warnings(src):\n\tvar prev_native_override_value = null\n\tvar native_method_override = 'debug/gdscript/warnings/native_method_override'\n\tprev_native_override_value = ProjectSettings.get_setting(native_method_override)\n\tProjectSettings.set_setting(native_method_override, 0)\n\n\tvar DblClass = GutUtils.create_script_from_source(src)\n\n\tProjectSettings.set_setting(native_method_override, prev_native_override_value)\n\treturn DblClass\n\n\nfunc _create_double(parsed, strategy, override_path, partial):\n\tvar path = \"\"\n\n\tpath = parsed.script_path\n\tvar dbl_src = \"\"\n\tvar included_methods = []\n\n\tfor method in parsed.get_local_methods():\n\t\tif(_is_method_eligible_for_doubling(parsed, method)):\n\t\t\tincluded_methods.append(method.meta.name)\n\t\t\tvar mthd = parsed.get_local_method(method.meta.name)\n\t\t\tif(parsed.is_native):\n\t\t\t\tdbl_src += _get_func_text(method.meta, parsed.resource)\n\t\t\telse:\n\t\t\t\tdbl_src += _get_func_text(method.meta, path)\n\n\tif(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE):\n\t\tfor method in parsed.get_super_methods():\n\t\t\tif(_is_method_eligible_for_doubling(parsed, method)):\n\t\t\t\tincluded_methods.append(method.meta.name)\n\t\t\t\t_stub_to_call_super(parsed, method.meta.name)\n\t\t\t\tif(parsed.is_native):\n\t\t\t\t\tdbl_src += _get_func_text(method.meta, parsed.resource)\n\t\t\t\telse:\n\t\t\t\t\tdbl_src += _get_func_text(method.meta, path)\n\n\tvar base_script = _get_base_script_text(parsed, override_path, partial, included_methods)\n\tdbl_src = base_script + \"\\n\\n\" + dbl_src\n\n\n\tif(print_source):\n\t\tprint(GutUtils.add_line_numbers(dbl_src))\n\n\tvar DblClass = _create_script_no_warnings(dbl_src)\n\tif(_stubber != null):\n\t\t_stub_method_default_values(DblClass, parsed, strategy)\n\n\treturn DblClass\n\n\nfunc _stub_method_default_values(which, parsed, strategy):\n\tfor method in parsed.get_local_methods():\n\t\tif(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)):\n\t\t\t_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)\n\n\n\nfunc _double_scene_and_script(scene, strategy, partial):\n\tvar to_return = PackedSceneDouble.new()\n\tto_return.load_scene(scene.get_path())\n\n\tvar script_obj = GutUtils.get_scene_script_object(scene)\n\tif(script_obj != null):\n\t\tvar script_dbl = null\n\t\tif(partial):\n\t\t\tscript_dbl = _partial_double(script_obj, strategy, scene.get_path())\n\t\telse:\n\t\t\tscript_dbl = _double(script_obj, strategy, scene.get_path())\n\t\tto_return.set_script_obj(script_dbl)\n\n\treturn to_return\n\n\nfunc _get_inst_id_ref_str(inst):\n\tvar ref_str = 'null'\n\tif(inst):\n\t\tref_str = str('instance_from_id(', inst.get_instance_id(),')')\n\treturn ref_str\n\n\nfunc _get_func_text(method_hash, path):\n\tvar override_count = null;\n\tif(_stubber != null):\n\t\toverride_count = _stubber.get_parameter_count(path, method_hash.name)\n\n\tvar text = _method_maker.get_function_text(method_hash, override_count) + \"\\n\"\n\n\treturn text\n\n\nfunc _parse_script(obj):\n\tvar parsed = null\n\n\tif(GutUtils.is_inner_class(obj)):\n\t\tif(inner_class_registry.has(obj)):\n\t\t\tparsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj)\n\t\telse:\n\t\t\t_lgr.error('Doubling Inner Classes requires you register them first.  Call register_inner_classes passing the script that contains the inner class.')\n\telse:\n\t\tparsed = _script_collector.parse(obj)\n\n\treturn parsed\n\n\n# Override path is used with scenes.\nfunc _double(obj, strategy, override_path=null):\n\tvar parsed = _parse_script(obj)\n\tif(parsed != null):\n\t\treturn _create_double(parsed, strategy, override_path, false)\n\n\nfunc _partial_double(obj, strategy, override_path=null):\n\tvar parsed = _parse_script(obj)\n\tif(parsed != null):\n\t\treturn _create_double(parsed, strategy, override_path, true)\n\n\n# -------------------------\n# Public\n# -------------------------\n\n# double a script/object\nfunc double(obj, strategy=_strategy):\n\treturn _double(obj, strategy)\n\nfunc partial_double(obj, strategy=_strategy):\n\treturn _partial_double(obj, strategy)\n\n\n# double a scene\nfunc double_scene(scene, strategy=_strategy):\n\treturn _double_scene_and_script(scene, strategy, false)\n\nfunc partial_double_scene(scene, strategy=_strategy):\n\treturn _double_scene_and_script(scene, strategy, true)\n\n\nfunc double_gdnative(which):\n\treturn _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)\n\nfunc partial_double_gdnative(which):\n\treturn _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)\n\n\nfunc double_inner(parent, inner, strategy=_strategy):\n\tvar parsed = _script_collector.parse(parent, inner)\n\treturn _create_double(parsed, strategy, null, false)\n\n\nfunc partial_double_inner(parent, inner, strategy=_strategy):\n\tvar parsed = _script_collector.parse(parent, inner)\n\treturn _create_double(parsed, strategy, null, true)\n\n\nfunc add_ignored_method(obj, method_name):\n\t_ignored_methods.add(obj, method_name)\n\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2024 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/doubler.gd.uid",
    "content": "uid://1a6kkk2dlt2i\n"
  },
  {
    "path": "addons/gut/dynamic_gdscript.gd",
    "content": "@tool\nvar default_script_name_no_extension = 'gut_dynamic_script'\nvar default_script_resource_path = 'res://addons/gut/not_a_real_file/'\n\nvar _created_script_count = 0\n\n\n# Creates a loaded script from the passed in source.  This loaded script is\n# returned unless there is an error.  When an error occcurs the error number\n# is returned instead.\nfunc create_script_from_source(source, override_path=null):\n\t_created_script_count += 1\n\tvar r_path = str(default_script_resource_path, default_script_name_no_extension, '_', _created_script_count)\n\tif(override_path != null):\n\t\tr_path = override_path\n\n\tvar DynamicScript = GDScript.new()\n\tDynamicScript.source_code = source.dedent()\n\t# The resource_path must be unique or Godot thinks it is trying\n\t# to load something it has already loaded and generates an error like\n\t# ERROR: Another resource is loaded from path 'workaround for godot\n\t# issue #65263' (possible cyclic resource inclusion).\n\tDynamicScript.resource_path = r_path\n\tvar result = DynamicScript.reload()\n\tif(result != OK):\n\t\tDynamicScript = result\n\n\treturn DynamicScript\n\n"
  },
  {
    "path": "addons/gut/dynamic_gdscript.gd.uid",
    "content": "uid://rh3x2vl3tmb2\n"
  },
  {
    "path": "addons/gut/fonts/AnonymousPro-Bold.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://c8axnpxc0nrk4\"\npath=\"res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/AnonymousPro-Bold.ttf\"\ndest_files=[\"res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://msst1l2s2s\"\npath=\"res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/AnonymousPro-BoldItalic.ttf\"\ndest_files=[\"res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/AnonymousPro-Italic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://hf5rdg67jcwc\"\npath=\"res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/AnonymousPro-Italic.ttf\"\ndest_files=[\"res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/AnonymousPro-Regular.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://c6c7gnx36opr0\"\npath=\"res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/AnonymousPro-Regular.ttf\"\ndest_files=[\"res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/CourierPrime-Bold.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bhjgpy1dovmyq\"\npath=\"res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/CourierPrime-Bold.ttf\"\ndest_files=[\"res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/CourierPrime-BoldItalic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://n6mxiov5sbgc\"\npath=\"res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/CourierPrime-BoldItalic.ttf\"\ndest_files=[\"res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/CourierPrime-Italic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://mcht266g817e\"\npath=\"res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/CourierPrime-Italic.ttf\"\ndest_files=[\"res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/CourierPrime-Regular.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bnh0lslf4yh87\"\npath=\"res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/CourierPrime-Regular.ttf\"\ndest_files=[\"res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/LobsterTwo-Bold.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://cmiuntu71oyl3\"\npath=\"res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/LobsterTwo-Bold.ttf\"\ndest_files=[\"res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bll38n2ct6qme\"\npath=\"res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/LobsterTwo-BoldItalic.ttf\"\ndest_files=[\"res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/LobsterTwo-Italic.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://dis65h8wxc3f2\"\npath=\"res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/LobsterTwo-Italic.ttf\"\ndest_files=[\"res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/LobsterTwo-Regular.ttf.import",
    "content": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://5e8msj0ih2pv\"\npath=\"res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/fonts/LobsterTwo-Regular.ttf\"\ndest_files=[\"res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata\"]\n\n[params]\n\nRendering=null\nantialiasing=1\ngenerate_mipmaps=false\ndisable_embedded_bitmaps=true\nmultichannel_signed_distance_field=false\nmsdf_pixel_range=8\nmsdf_size=48\nallow_system_fallback=true\nforce_autohinter=false\nhinting=1\nsubpixel_positioning=1\nkeep_rounding_remainders=true\noversampling=0.0\nFallbacks=null\nfallbacks=[]\nCompress=null\ncompress=true\npreload=[]\nlanguage_support={}\nscript_support={}\nopentype_features={}\n"
  },
  {
    "path": "addons/gut/fonts/OFL.txt",
    "content": "Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com),\nwith Reserved Font Name Anonymous Pro.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "addons/gut/gui/BottomPanelShortcuts.gd",
    "content": "@tool\nextends Window\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar default_path = GutEditorGlobals.editor_shortcuts_path\n\n@onready var _ctrls = {\n\trun_all = $Layout/CRunAll/ShortcutButton,\n\trun_current_script = $Layout/CRunCurrentScript/ShortcutButton,\n\trun_current_inner = $Layout/CRunCurrentInner/ShortcutButton,\n\trun_current_test = $Layout/CRunCurrentTest/ShortcutButton,\n\tpanel_button = $Layout/CPanelButton/ShortcutButton,\n}\n\nvar _user_prefs = GutEditorGlobals.user_prefs\n\nfunc _ready():\n\tfor key in _ctrls:\n\t\tvar sc_button = _ctrls[key]\n\t\tsc_button.connect('start_edit', _on_edit_start.bind(sc_button))\n\t\tsc_button.connect('end_edit', _on_edit_end)\n\n\t# show dialog when running scene from editor.\n\tif(get_parent() == get_tree().root):\n\t\tpopup_centered()\n\n\nfunc _cancel_all():\n\t_ctrls.run_all.cancel()\n\t_ctrls.run_current_script.cancel()\n\t_ctrls.run_current_inner.cancel()\n\t_ctrls.run_current_test.cancel()\n\t_ctrls.panel_button.cancel()\n\n# ------------\n# Events\n# ------------\nfunc _on_Hide_pressed():\n\thide()\n\n\nfunc _on_edit_start(which):\n\tfor key in _ctrls:\n\t\tvar sc_button = _ctrls[key]\n\t\tif(sc_button != which):\n\t\t\tsc_button.disable_set(true)\n\t\t\tsc_button.disable_clear(true)\n\n\nfunc _on_edit_end():\n\tfor key in _ctrls:\n\t\tvar sc_button = _ctrls[key]\n\t\tsc_button.disable_set(false)\n\t\tsc_button.disable_clear(false)\n\n\nfunc _on_popup_hide():\n\t_cancel_all()\n\n# ------------\n# Public\n# ------------\nfunc get_run_all():\n\treturn _ctrls.run_all.get_shortcut()\n\nfunc get_run_current_script():\n\treturn _ctrls.run_current_script.get_shortcut()\n\nfunc get_run_current_inner():\n\treturn _ctrls.run_current_inner.get_shortcut()\n\nfunc get_run_current_test():\n\treturn _ctrls.run_current_test.get_shortcut()\n\nfunc get_panel_button():\n\treturn _ctrls.panel_button.get_shortcut()\n\nfunc _set_pref_value(pref, button):\n\tpref.value = {shortcut = button.get_shortcut().events}\n\n\nfunc save_shortcuts():\n\tsave_shortcuts_to_file(default_path)\n\n\nfunc save_shortcuts_to_editor_settings():\n\t_set_pref_value(_user_prefs.shortcut_run_all, _ctrls.run_all)\n\t_set_pref_value(_user_prefs.shortcut_run_current_script, _ctrls.run_current_script)\n\t_set_pref_value(_user_prefs.shortcut_run_current_inner, _ctrls.run_current_inner)\n\t_set_pref_value(_user_prefs.shortcut_run_current_test, _ctrls.run_current_test)\n\t_set_pref_value(_user_prefs.shortcut_panel_button, _ctrls.panel_button)\n\n\t_user_prefs.save_it()\n\n\nfunc save_shortcuts_to_file(path):\n\tvar f = ConfigFile.new()\n\tf.set_value('main', 'run_all', _ctrls.run_all.get_shortcut())\n\tf.set_value('main', 'run_current_script', _ctrls.run_current_script.get_shortcut())\n\tf.set_value('main', 'run_current_inner', _ctrls.run_current_inner.get_shortcut())\n\tf.set_value('main', 'run_current_test', _ctrls.run_current_test.get_shortcut())\n\tf.set_value('main', 'panel_button', _ctrls.panel_button.get_shortcut())\n\tf.save(path)\n\n\nfunc _load_shortcut_from_pref(user_pref):\n\tvar to_return = Shortcut.new()\n\t# value with be _user_prefs.EMPTY which is a string when the value\n\t# has not been set.\n\tif(typeof(user_pref.value) == TYPE_DICTIONARY):\n\t\tto_return.events.append(user_pref.value.shortcut[0])\n\t\t# to_return = user_pref.value\n\treturn to_return\n\n\nfunc load_shortcuts():\n\tload_shortcuts_from_file(default_path)\n\n\nfunc load_shortcuts_from_editor_settings():\n\tvar empty = Shortcut.new()\n\n\t_ctrls.run_all.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_all))\n\t_ctrls.run_current_script.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_script))\n\t_ctrls.run_current_inner.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_inner))\n\t_ctrls.run_current_test.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_test))\n\t_ctrls.panel_button.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_panel_button))\n\n\nfunc load_shortcuts_from_file(path):\n\tvar f = ConfigFile.new()\n\tvar empty = Shortcut.new()\n\n\tf.load(path)\n\t_ctrls.run_all.set_shortcut(f.get_value('main', 'run_all', empty))\n\t_ctrls.run_current_script.set_shortcut(f.get_value('main', 'run_current_script', empty))\n\t_ctrls.run_current_inner.set_shortcut(f.get_value('main', 'run_current_inner', empty))\n\t_ctrls.run_current_test.set_shortcut(f.get_value('main', 'run_current_test', empty))\n\t_ctrls.panel_button.set_shortcut(f.get_value('main', 'panel_button', empty))\n\n\n"
  },
  {
    "path": "addons/gut/gui/BottomPanelShortcuts.gd.uid",
    "content": "uid://ck68kf4ewmrrf\n"
  },
  {
    "path": "addons/gut/gui/BottomPanelShortcuts.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://bsk32dh41b4gs\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://sfb1fw8j6ufu\" path=\"res://addons/gut/gui/ShortcutButton.tscn\" id=\"1\"]\n[ext_resource type=\"Script\" uid=\"uid://ck68kf4ewmrrf\" path=\"res://addons/gut/gui/BottomPanelShortcuts.gd\" id=\"2\"]\n\n[node name=\"BottomPanelShortcuts\" type=\"Popup\"]\ntitle = \"Shortcuts\"\nsize = Vector2i(500, 350)\nvisible = true\nexclusive = true\nunresizable = false\nborderless = false\nscript = ExtResource(\"2\")\n\n[node name=\"Layout\" type=\"VBoxContainer\" parent=\".\"]\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_left = 5.0\noffset_right = -5.0\noffset_bottom = 2.0\n\n[node name=\"TopPad\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 5)\nlayout_mode = 2\n\n[node name=\"Label2\" type=\"Label\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 20)\nlayout_mode = 2\ntext = \"Always Active\"\n\n[node name=\"ColorRect\" type=\"ColorRect\" parent=\"Layout/Label2\"]\nshow_behind_parent = true\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\ncolor = Color(0, 0, 0, 0.196078)\n\n[node name=\"CPanelButton\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout/CPanelButton\"]\ncustom_minimum_size = Vector2(50, 0)\nlayout_mode = 2\nsize_flags_vertical = 7\ntext = \"Show/Hide GUT Panel\"\n\n[node name=\"ShortcutButton\" parent=\"Layout/CPanelButton\" instance=ExtResource(\"1\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"GutPanelPad\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 5)\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 20)\nlayout_mode = 2\ntext = \"Only Active When GUT Panel Shown\"\n\n[node name=\"ColorRect2\" type=\"ColorRect\" parent=\"Layout/Label\"]\nshow_behind_parent = true\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\ncolor = Color(0, 0, 0, 0.196078)\n\n[node name=\"TopPad2\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 5)\nlayout_mode = 2\n\n[node name=\"CRunAll\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout/CRunAll\"]\ncustom_minimum_size = Vector2(50, 0)\nlayout_mode = 2\nsize_flags_vertical = 7\ntext = \"Run All\"\n\n[node name=\"ShortcutButton\" parent=\"Layout/CRunAll\" instance=ExtResource(\"1\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"CRunCurrentScript\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout/CRunCurrentScript\"]\ncustom_minimum_size = Vector2(50, 0)\nlayout_mode = 2\nsize_flags_vertical = 7\ntext = \"Run Current Script\"\n\n[node name=\"ShortcutButton\" parent=\"Layout/CRunCurrentScript\" instance=ExtResource(\"1\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"CRunCurrentInner\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout/CRunCurrentInner\"]\ncustom_minimum_size = Vector2(50, 0)\nlayout_mode = 2\nsize_flags_vertical = 7\ntext = \"Run Current Inner Class\"\n\n[node name=\"ShortcutButton\" parent=\"Layout/CRunCurrentInner\" instance=ExtResource(\"1\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"CRunCurrentTest\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"Layout/CRunCurrentTest\"]\ncustom_minimum_size = Vector2(50, 0)\nlayout_mode = 2\nsize_flags_vertical = 7\ntext = \"Run Current Test\"\n\n[node name=\"ShortcutButton\" parent=\"Layout/CRunCurrentTest\" instance=ExtResource(\"1\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"CenterContainer2\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 5)\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"ShiftDisclaimer\" type=\"Label\" parent=\"Layout\"]\nlayout_mode = 2\ntext = \"\\\"Shift\\\" cannot be the only modifier for a shortcut.\"\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"Layout\"]\nlayout_mode = 2\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"Layout/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"Hide\" type=\"Button\" parent=\"Layout/HBoxContainer\"]\ncustom_minimum_size = Vector2(60, 30)\nlayout_mode = 2\ntext = \"Close\"\n\n[node name=\"BottomPad\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(0, 10)\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[connection signal=\"popup_hide\" from=\".\" to=\".\" method=\"_on_popup_hide\"]\n[connection signal=\"pressed\" from=\"Layout/HBoxContainer/Hide\" to=\".\" method=\"_on_Hide_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/GutBottomPanel.gd",
    "content": "@tool\nextends Control\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar TestScript = load('res://addons/gut/test.gd')\nvar GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')\nvar ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')\n\n\nvar _interface = null;\nvar _is_running = false;\nvar _gut_config = load('res://addons/gut/gut_config.gd').new()\nvar _gut_config_gui = null\nvar _gut_plugin = null\nvar _light_color = Color(0, 0, 0, .5)\nvar _panel_button = null\nvar _last_selected_path = null\nvar _user_prefs = null\n\n\n@onready var _ctrls = {\n\toutput = $layout/RSplit/CResults/TabBar/OutputText.get_rich_text_edit(),\n\toutput_ctrl = $layout/RSplit/CResults/TabBar/OutputText,\n\trun_button = $layout/ControlBar/RunAll,\n\tshortcuts_button = $layout/ControlBar/Shortcuts,\n\n\tsettings_button = $layout/ControlBar/Settings,\n\trun_results_button = $layout/ControlBar/RunResultsBtn,\n\toutput_button = $layout/ControlBar/OutputBtn,\n\n\tsettings = $layout/RSplit/sc/Settings,\n\tshortcut_dialog = $BottomPanelShortcuts,\n\tlight = $layout/RSplit/CResults/ControlBar/Light3D,\n\tresults = {\n\t\tbar = $layout/RSplit/CResults/ControlBar,\n\t\tpassing = $layout/RSplit/CResults/ControlBar/Passing/value,\n\t\tfailing = $layout/RSplit/CResults/ControlBar/Failing/value,\n\t\tpending = $layout/RSplit/CResults/ControlBar/Pending/value,\n\t\terrors = $layout/RSplit/CResults/ControlBar/Errors/value,\n\t\twarnings = $layout/RSplit/CResults/ControlBar/Warnings/value,\n\t\torphans = $layout/RSplit/CResults/ControlBar/Orphans/value\n\t},\n\trun_at_cursor = $layout/ControlBar/RunAtCursor,\n\trun_results = $layout/RSplit/CResults/TabBar/RunResults\n}\n\nfunc _init():\n\tpass\n\n\nfunc _ready():\n\tGutEditorGlobals.create_temp_directory()\n\n\t_user_prefs = GutEditorGlobals.user_prefs\n\t_gut_config_gui = GutConfigGui.new(_ctrls.settings)\n\n\t_ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar))\n\thide_settings(!_ctrls.settings_button.button_pressed)\n\n\t_gut_config.load_options(GutEditorGlobals.editor_run_gut_config_path)\n\t_gut_config_gui.set_options(_gut_config.options)\n\t_apply_options_to_controls()\n\n\t_ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons')\n\t_ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons')\n\t_ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree\n\t_ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons')\n\n\t_ctrls.run_results.set_output_control(_ctrls.output_ctrl)\n\n\tvar check_import = load('res://addons/gut/images/red.png')\n\tif(check_import == null):\n\t\t_ctrls.run_results.add_centered_text(\"GUT got some new images that are not imported yet.  Please restart Godot.\")\n\t\tprint('GUT got some new images that are not imported yet.  Please restart Godot.')\n\telse:\n\t\t_ctrls.run_results.add_centered_text(\"Let's run some tests!\")\n\n\nfunc _apply_options_to_controls():\n\thide_settings(_user_prefs.hide_settings.value)\n\thide_result_tree(_user_prefs.hide_result_tree.value)\n\thide_output_text(_user_prefs.hide_output_text.value)\n\t_ctrls.run_results.set_show_orphans(!_gut_config.options.hide_orphans)\n\n\nfunc _process(delta):\n\tif(_is_running):\n\t\tif(!_interface.is_playing_scene()):\n\t\t\t_is_running = false\n\t\t\t_ctrls.output_ctrl.add_text(\"\\ndone\")\n\t\t\tload_result_output()\n\t\t\t_gut_plugin.make_bottom_panel_item_visible(self)\n\n# ---------------\n# Private\n# ---------------\n\nfunc load_shortcuts():\n\t_ctrls.shortcut_dialog.load_shortcuts()\n\t_apply_shortcuts()\n\n\nfunc _is_test_script(script):\n\tvar from = script.get_base_script()\n\twhile(from and from.resource_path != 'res://addons/gut/test.gd'):\n\t\tfrom = from.get_base_script()\n\n\treturn from != null\n\n\nfunc _show_errors(errs):\n\t_ctrls.output_ctrl.clear()\n\tvar text = \"Cannot run tests, you have a configuration error:\\n\"\n\tfor e in errs:\n\t\ttext += str('*  ', e, \"\\n\")\n\ttext += \"Check your settings ----->\"\n\t_ctrls.output_ctrl.add_text(text)\n\thide_output_text(false)\n\thide_settings(false)\n\n\nfunc _save_config():\n\t_user_prefs.hide_settings.value = !_ctrls.settings_button.button_pressed\n\t_user_prefs.hide_result_tree.value = !_ctrls.run_results_button.button_pressed\n\t_user_prefs.hide_output_text.value = !_ctrls.output_button.button_pressed\n\t_user_prefs.save_it()\n\n\t_gut_config.options = _gut_config_gui.get_options(_gut_config.options)\n\tvar w_result = _gut_config.write_options(GutEditorGlobals.editor_run_gut_config_path)\n\tif(w_result != OK):\n\t\tpush_error(str('Could not write options to ', GutEditorGlobals.editor_run_gut_config_path, ': ', w_result))\n\telse:\n\t\t_gut_config_gui.mark_saved()\n\n\nfunc _run_tests():\n\tGutEditorGlobals.create_temp_directory()\n\n\tvar issues = _gut_config_gui.get_config_issues()\n\tif(issues.size() > 0):\n\t\t_show_errors(issues)\n\t\treturn\n\n\twrite_file(GutEditorGlobals.editor_run_bbcode_results_path, 'Run in progress')\n\t_save_config()\n\t_apply_options_to_controls()\n\n\t_ctrls.output_ctrl.clear()\n\t_ctrls.run_results.clear()\n\t_ctrls.run_results.add_centered_text('Running...')\n\n\t_interface.play_custom_scene('res://addons/gut/gui/GutRunner.tscn')\n\t_is_running = true\n\t_ctrls.output_ctrl.add_text('Running...')\n\n\nfunc _apply_shortcuts():\n\t_ctrls.run_button.shortcut = _ctrls.shortcut_dialog.get_run_all()\n\n\t_ctrls.run_at_cursor.get_script_button().shortcut = \\\n\t\t_ctrls.shortcut_dialog.get_run_current_script()\n\t_ctrls.run_at_cursor.get_inner_button().shortcut = \\\n\t\t_ctrls.shortcut_dialog.get_run_current_inner()\n\t_ctrls.run_at_cursor.get_test_button().shortcut = \\\n\t\t_ctrls.shortcut_dialog.get_run_current_test()\n\n\t_panel_button.shortcut = _ctrls.shortcut_dialog.get_panel_button()\n\n\nfunc _run_all():\n\t_gut_config.options.selected = null\n\t_gut_config.options.inner_class = null\n\t_gut_config.options.unit_test_name = null\n\n\t_run_tests()\n\n\n# ---------------\n# Events\n# ---------------\nfunc _on_results_bar_draw(bar):\n\tbar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2))\n\n\nfunc _on_Light_draw():\n\tvar l = _ctrls.light\n\tl.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color)\n\n\nfunc _on_editor_script_changed(script):\n\tif(script):\n\t\tset_current_script(script)\n\n\nfunc _on_RunAll_pressed():\n\t_run_all()\n\n\nfunc _on_Shortcuts_pressed():\n\t_ctrls.shortcut_dialog.popup_centered()\n\nfunc _on_bottom_panel_shortcuts_visibility_changed():\n\t_apply_shortcuts()\n\t_ctrls.shortcut_dialog.save_shortcuts()\n\nfunc _on_RunAtCursor_run_tests(what):\n\t_gut_config.options.selected = what.script\n\t_gut_config.options.inner_class = what.inner_class\n\t_gut_config.options.unit_test_name = what.test_method\n\n\t_run_tests()\n\n\nfunc _on_Settings_pressed():\n\thide_settings(!_ctrls.settings_button.button_pressed)\n\t_save_config()\n\n\nfunc _on_OutputBtn_pressed():\n\thide_output_text(!_ctrls.output_button.button_pressed)\n\t_save_config()\n\n\nfunc _on_RunResultsBtn_pressed():\n\thide_result_tree(! _ctrls.run_results_button.button_pressed)\n\t_save_config()\n\n\n# Currently not used, but will be when I figure out how to put\n# colors into the text results\nfunc _on_UseColors_pressed():\n\tpass\n\n# ---------------\n# Public\n# ---------------\nfunc hide_result_tree(should):\n\t_ctrls.run_results.visible = !should\n\t_ctrls.run_results_button.button_pressed = !should\n\n\nfunc hide_settings(should):\n\tvar s_scroll = _ctrls.settings.get_parent()\n\ts_scroll.visible = !should\n\n\t# collapse only collapses the first control, so we move\n\t# settings around to be the collapsed one\n\tif(should):\n\t\ts_scroll.get_parent().move_child(s_scroll, 0)\n\telse:\n\t\ts_scroll.get_parent().move_child(s_scroll, 1)\n\n\t$layout/RSplit.collapsed = should\n\t_ctrls.settings_button.button_pressed = !should\n\n\nfunc hide_output_text(should):\n\t$layout/RSplit/CResults/TabBar/OutputText.visible = !should\n\t_ctrls.output_button.button_pressed = !should\n\n\nfunc load_result_output():\n\t_ctrls.output_ctrl.load_file(GutEditorGlobals.editor_run_bbcode_results_path)\n\n\tvar summary = get_file_as_text(GutEditorGlobals.editor_run_json_results_path)\n\tvar test_json_conv = JSON.new()\n\tif (test_json_conv.parse(summary) != OK):\n\t\treturn\n\tvar results = test_json_conv.get_data()\n\n\t_ctrls.run_results.load_json_results(results)\n\n\tvar summary_json = results['test_scripts']['props']\n\t_ctrls.results.passing.text = str(summary_json.passing)\n\t_ctrls.results.passing.get_parent().visible = true\n\n\t_ctrls.results.failing.text = str(summary_json.failures)\n\t_ctrls.results.failing.get_parent().visible = true\n\n\t_ctrls.results.pending.text = str(summary_json.pending)\n\t_ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0'\n\n\t_ctrls.results.errors.text = str(summary_json.errors)\n\t_ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0'\n\n\t_ctrls.results.warnings.text = str(summary_json.warnings)\n\t_ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0'\n\n\t_ctrls.results.orphans.text = str(summary_json.orphans)\n\t_ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans\n\n\tif(summary_json.tests == 0):\n\t\t_light_color = Color(1, 0, 0, .75)\n\telif(summary_json.failures != 0):\n\t\t_light_color = Color(1, 0, 0, .75)\n\telif(summary_json.pending != 0):\n\t\t_light_color = Color(1, 1, 0, .75)\n\telse:\n\t\t_light_color = Color(0, 1, 0, .75)\n\t_ctrls.light.visible = true\n\t_ctrls.light.queue_redraw()\n\n\nfunc set_current_script(script):\n\tif(script):\n\t\tif(_is_test_script(script)):\n\t\t\tvar file = script.resource_path.get_file()\n\t\t\t_last_selected_path = script.resource_path.get_file()\n\t\t\t_ctrls.run_at_cursor.activate_for_script(script.resource_path)\n\n\nfunc set_interface(value):\n\t_interface = value\n\t_interface.get_script_editor().connect(\"editor_script_changed\",Callable(self,'_on_editor_script_changed'))\n\n\tvar ste = ScriptTextEditors.new(_interface.get_script_editor())\n\t_ctrls.run_results.set_interface(_interface)\n\t_ctrls.run_results.set_script_text_editors(ste)\n\t_ctrls.run_at_cursor.set_script_text_editors(ste)\n\tset_current_script(_interface.get_script_editor().get_current_script())\n\n\nfunc set_plugin(value):\n\t_gut_plugin = value\n\n\nfunc set_panel_button(value):\n\t_panel_button = value\n\n# ------------------------------------------------------------------------------\n# Write a file.\n# ------------------------------------------------------------------------------\nfunc write_file(path, content):\n\tvar f = FileAccess.open(path, FileAccess.WRITE)\n\tif(f != null):\n\t\tf.store_string(content)\n\tf = null;\n\n\treturn FileAccess.get_open_error()\n\n\n# ------------------------------------------------------------------------------\n# Returns the text of a file or an empty string if the file could not be opened.\n# ------------------------------------------------------------------------------\nfunc get_file_as_text(path):\n\tvar to_return = ''\n\tvar f = FileAccess.open(path, FileAccess.READ)\n\tif(f != null):\n\t\tto_return = f.get_as_text()\n\tf = null\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# return if_null if value is null otherwise return value\n# ------------------------------------------------------------------------------\nfunc nvl(value, if_null):\n\tif(value == null):\n\t\treturn if_null\n\telse:\n\t\treturn value\n"
  },
  {
    "path": "addons/gut/gui/GutBottomPanel.gd.uid",
    "content": "uid://bhjmhcm3b7dv2\n"
  },
  {
    "path": "addons/gut/gui/GutBottomPanel.tscn",
    "content": "[gd_scene load_steps=10 format=3 uid=\"uid://b3bostcslstem\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bhjmhcm3b7dv2\" path=\"res://addons/gut/gui/GutBottomPanel.gd\" id=\"1\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://bsk32dh41b4gs\" path=\"res://addons/gut/gui/BottomPanelShortcuts.tscn\" id=\"2\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://0yunjxtaa8iw\" path=\"res://addons/gut/gui/RunAtCursor.tscn\" id=\"3\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cr6tvdv0ve6cv\" path=\"res://addons/gut/gui/play.png\" id=\"4\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://4gyyn12um08h\" path=\"res://addons/gut/gui/RunResults.tscn\" id=\"5\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://bqmo4dj64c7yl\" path=\"res://addons/gut/gui/OutputText.tscn\" id=\"6\"]\n\n[sub_resource type=\"Shortcut\" id=\"9\"]\n\n[sub_resource type=\"Image\" id=\"Image_p7oqn\"]\ndata = {\n\"data\": PackedByteArray(255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 42, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 93, 93, 41, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 92, 92, 0, 255, 92, 92, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 93, 93, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0),\n\"format\": \"RGBA8\",\n\"height\": 16,\n\"mipmaps\": false,\n\"width\": 16\n}\n\n[sub_resource type=\"ImageTexture\" id=\"ImageTexture_srqj5\"]\nimage = SubResource(\"Image_p7oqn\")\n\n[node name=\"GutBottomPanel\" type=\"Control\"]\ncustom_minimum_size = Vector2(250, 250)\nlayout_mode = 3\nanchor_left = -0.0025866\nanchor_top = -0.00176575\nanchor_right = 0.997413\nanchor_bottom = 0.998234\noffset_left = 2.64868\noffset_top = 1.05945\noffset_right = 2.64862\noffset_bottom = 1.05945\nscript = ExtResource(\"1\")\n\n[node name=\"layout\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\n\n[node name=\"ControlBar\" type=\"HBoxContainer\" parent=\"layout\"]\nlayout_mode = 2\n\n[node name=\"RunAll\" type=\"Button\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\nsize_flags_vertical = 11\nshortcut = SubResource(\"9\")\ntext = \"Run All\"\nicon = ExtResource(\"4\")\n\n[node name=\"Label\" type=\"Label\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\nmouse_filter = 1\ntext = \"Current:  \"\n\n[node name=\"RunAtCursor\" parent=\"layout/ControlBar\" instance=ExtResource(\"3\")]\nlayout_mode = 2\n\n[node name=\"CenterContainer2\" type=\"CenterContainer\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Sep1\" type=\"ColorRect\" parent=\"layout/ControlBar\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"RunResultsBtn\" type=\"Button\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"OutputBtn\" type=\"Button\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Settings\" type=\"Button\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Sep2\" type=\"ColorRect\" parent=\"layout/ControlBar\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"Shortcuts\" type=\"Button\" parent=\"layout/ControlBar\"]\nlayout_mode = 2\nsize_flags_vertical = 11\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"RSplit\" type=\"HSplitContainer\" parent=\"layout\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"CResults\" type=\"VBoxContainer\" parent=\"layout/RSplit\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"ControlBar\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults\"]\nlayout_mode = 2\n\n[node name=\"Sep2\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"Light3D\" type=\"Control\" parent=\"layout/RSplit/CResults/ControlBar\"]\ncustom_minimum_size = Vector2(30, 30)\nlayout_mode = 2\n\n[node name=\"Passing\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Passing\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Passing\"]\nlayout_mode = 2\ntext = \"Passing\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Passing\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"Failing\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Failing\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Failing\"]\nlayout_mode = 2\ntext = \"Failing\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Failing\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"Pending\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Pending\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Pending\"]\nlayout_mode = 2\ntext = \"Pending\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Pending\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"Orphans\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Orphans\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Orphans\"]\nlayout_mode = 2\ntext = \"Orphans\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Orphans\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"Errors\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Errors\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Errors\"]\nlayout_mode = 2\ntext = \"Errors\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Errors\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"Warnings\" type=\"HBoxContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"layout/RSplit/CResults/ControlBar/Warnings\"]\ncustom_minimum_size = Vector2(1, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"label\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Warnings\"]\nlayout_mode = 2\ntext = \"Warnings\"\n\n[node name=\"value\" type=\"Label\" parent=\"layout/RSplit/CResults/ControlBar/Warnings\"]\nlayout_mode = 2\ntext = \"---\"\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"layout/RSplit/CResults/ControlBar\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"TabBar\" type=\"HSplitContainer\" parent=\"layout/RSplit/CResults\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"RunResults\" parent=\"layout/RSplit/CResults/TabBar\" instance=ExtResource(\"5\")]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"OutputText\" parent=\"layout/RSplit/CResults/TabBar\" instance=ExtResource(\"6\")]\nlayout_mode = 2\n\n[node name=\"sc\" type=\"ScrollContainer\" parent=\"layout/RSplit\"]\ncustom_minimum_size = Vector2(500, 2.08165e-12)\nlayout_mode = 2\nsize_flags_vertical = 3\n\n[node name=\"Settings\" type=\"VBoxContainer\" parent=\"layout/RSplit/sc\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"BottomPanelShortcuts\" parent=\".\" instance=ExtResource(\"2\")]\nvisible = false\n\n[connection signal=\"pressed\" from=\"layout/ControlBar/RunAll\" to=\".\" method=\"_on_RunAll_pressed\"]\n[connection signal=\"run_tests\" from=\"layout/ControlBar/RunAtCursor\" to=\".\" method=\"_on_RunAtCursor_run_tests\"]\n[connection signal=\"pressed\" from=\"layout/ControlBar/RunResultsBtn\" to=\".\" method=\"_on_RunResultsBtn_pressed\"]\n[connection signal=\"pressed\" from=\"layout/ControlBar/OutputBtn\" to=\".\" method=\"_on_OutputBtn_pressed\"]\n[connection signal=\"pressed\" from=\"layout/ControlBar/Settings\" to=\".\" method=\"_on_Settings_pressed\"]\n[connection signal=\"pressed\" from=\"layout/ControlBar/Shortcuts\" to=\".\" method=\"_on_Shortcuts_pressed\"]\n[connection signal=\"draw\" from=\"layout/RSplit/CResults/ControlBar/Light3D\" to=\".\" method=\"_on_Light_draw\"]\n[connection signal=\"visibility_changed\" from=\"BottomPanelShortcuts\" to=\".\" method=\"_on_bottom_panel_shortcuts_visibility_changed\"]\n"
  },
  {
    "path": "addons/gut/gui/GutControl.gd",
    "content": "@tool\nextends Control\n\nconst RUNNER_JSON_PATH = 'res://.gut_editor_config.json'\n\nvar GutConfig = load('res://addons/gut/gut_config.gd')\nvar GutRunnerScene = load('res://addons/gut/gui/GutRunner.tscn')\nvar GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')\n\nvar _config = GutConfig.new()\nvar _config_gui = null\nvar _gut_runner = GutRunnerScene.instantiate()\nvar _has_connected = false\nvar _tree_root : TreeItem = null\n\nvar _script_icon = load('res://addons/gut/images/Script.svg')\nvar _folder_icon = load('res://addons/gut/images/Folder.svg')\n\nvar _tree_scripts = {}\nvar _tree_directories = {}\n\nconst TREE_SCRIPT = 'Script'\nconst TREE_DIR = 'Directory'\n\n@onready var _ctrls = {\n\trun_tests_button = $VBox/Buttons/RunTests,\n\trun_selected = $VBox/Buttons/RunSelected,\n\ttest_tree = $VBox/Tabs/Tests,\n\tsettings_vbox = $VBox/Tabs/SettingsScroll/Settings,\n\ttabs = $VBox/Tabs,\n\tbg = $Bg\n}\n\n@export var bg_color : Color = Color(.36, .36, .36) :\n\tget: return bg_color\n\tset(val):\n\t\tbg_color = val\n\t\tif(is_inside_tree()):\n\t\t\t$Bg.color = bg_color\n\n\nfunc _ready():\n\tif Engine.is_editor_hint():\n\t\treturn\n\n\t$Bg.color = bg_color\n\t_ctrls.tabs.set_tab_title(0, 'Tests')\n\t_ctrls.tabs.set_tab_title(1, 'Settings')\n\n\t_config_gui = GutConfigGui.new(_ctrls.settings_vbox)\n\n\t_ctrls.test_tree.hide_root = true\n\t# Stop tests from kicking off when the runner is \"ready\" and prevents it\n\t# from writing results file that is used by the panel.\n\t_gut_runner.ran_from_editor = false\n\tadd_child(_gut_runner)\n\n\t# TODO This might not need to be called deferred after changing GutUtils to\n\t# an all static class.\n\tcall_deferred('_post_ready')\n\n\nfunc _draw():\n\tif Engine.is_editor_hint():\n\t\treturn\n\n\tvar gut = _gut_runner.get_gut()\n\tif(!gut.is_running()):\n\t\tvar r = Rect2(Vector2(0, 0), get_rect().size)\n\t\tdraw_rect(r, Color.BLACK, false, 2)\n\n\nfunc _post_ready():\n\tvar gut = _gut_runner.get_gut()\n\tgut.start_run.connect(_on_gut_run_started)\n\tgut.end_run.connect(_on_gut_run_ended)\n\t_refresh_tree_and_settings()\n\n\nfunc _set_meta_for_script_tree_item(item, script, test=null):\n\tvar meta = {\n\t\ttype = TREE_SCRIPT,\n\t\tscript = script.path,\n\t\tinner_class = script.inner_class_name,\n\t\ttest = ''\n\t}\n\n\tif(test != null):\n\t\tmeta.test = test.name\n\n\titem.set_metadata(0, meta)\n\n\nfunc _set_meta_for_directory_tree_item(item, path, temp_item):\n\tvar meta = {\n\t\ttype = TREE_DIR,\n\t\tpath = path,\n\t\ttemp_item = temp_item\n\t}\n\titem.set_metadata(0, meta)\n\n\nfunc _get_script_tree_item(script, parent_item):\n\tif(!_tree_scripts.has(script.path)):\n\t\tvar item = _ctrls.test_tree.create_item(parent_item)\n\t\titem.set_text(0, script.path.get_file())\n\t\titem.set_icon(0, _script_icon)\n\t\t_tree_scripts[script.path] = item\n\t\t_set_meta_for_script_tree_item(item, script)\n\n\treturn _tree_scripts[script.path]\n\n\nfunc _get_directory_tree_item(path):\n\tvar parent = _tree_root\n\tif(!_tree_directories.has(path)):\n\n\t\tvar item : TreeItem = null\n\t\tif(parent != _tree_root):\n\t\t\titem = parent.create_child(0)\n\t\telse:\n\t\t\titem = parent.create_child()\n\n\t\t_tree_directories[path] = item\n\t\titem.collapsed = false\n\t\titem.set_text(0, path)\n\t\titem.set_icon(0, _folder_icon)\n\t\titem.set_icon_modulate(0, Color.ROYAL_BLUE)\n\t\t# temp_item is used in calls with move_before since you must use\n\t\t# move_before or move_after to reparent tree items. This ensures that\n\t\t# there is an item on all directories.  These are deleted later.\n\t\tvar temp_item = item.create_child()\n\t\ttemp_item.set_text(0, '<temp>')\n\n\t\t_set_meta_for_directory_tree_item(item, path, temp_item)\n\n\treturn _tree_directories[path]\n\n\nfunc _find_dir_item_to_move_before(path):\n\tvar max_matching_len = 0\n\tvar best_parent = null\n\n\t# Go through all the directory items finding the one that has the longest\n\t# path that contains our path.\n\tfor key in _tree_directories.keys():\n\t\tif(path != key and path.begins_with(key) and key.length() > max_matching_len):\n\t\t\t\tmax_matching_len = key.length()\n\t\t\t\tbest_parent = _tree_directories[key]\n\n\tvar to_return = null\n\tif(best_parent != null):\n\t\tto_return = best_parent.get_metadata(0).temp_item\n\treturn to_return\n\n\nfunc _reorder_dir_items():\n\tvar the_keys = _tree_directories.keys()\n\tthe_keys.sort()\n\tfor key in _tree_directories.keys():\n\t\tvar to_move = _tree_directories[key]\n\t\tto_move.collapsed = false\n\t\tvar move_before = _find_dir_item_to_move_before(key)\n\t\tif(move_before != null):\n\t\t\tto_move.move_before(move_before)\n\t\t\tvar new_text = key.substr(move_before.get_parent().get_metadata(0).path.length())\n\t\t\tto_move.set_text(0, new_text)\n\n\nfunc _remove_dir_temp_items():\n\tfor key in _tree_directories.keys():\n\t\tvar item = _tree_directories[key].get_metadata(0).temp_item\n\t\t_tree_directories[key].remove_child(item)\n\n\nfunc _add_dir_and_script_tree_items():\n\tvar tree : Tree = _ctrls.test_tree\n\ttree.clear()\n\t_tree_root = _ctrls.test_tree.create_item()\n\n\tvar scripts = _gut_runner.get_gut().get_test_collector().scripts\n\tfor script in scripts:\n\t\tvar dir_item = _get_directory_tree_item(script.path.get_base_dir())\n\t\tvar item = _get_script_tree_item(script, dir_item)\n\n\t\tif(script.inner_class_name != ''):\n\t\t\tvar inner_item = tree.create_item(item)\n\t\t\tinner_item.set_text(0, script.inner_class_name)\n\t\t\t_set_meta_for_script_tree_item(inner_item, script)\n\t\t\titem = inner_item\n\n\t\tfor test in script.tests:\n\t\t\tvar test_item = tree.create_item(item)\n\t\t\ttest_item.set_text(0, test.name)\n\t\t\t_set_meta_for_script_tree_item(test_item, script, test)\n\n\nfunc _populate_tree():\n\t_add_dir_and_script_tree_items()\n\t_tree_root.set_collapsed_recursive(true)\n\t_tree_root.set_collapsed(false)\n\t_reorder_dir_items()\n\t_remove_dir_temp_items()\n\n\nfunc _refresh_tree_and_settings():\n\t_config.apply_options(_gut_runner.get_gut())\n\t_gut_runner.set_gut_config(_config)\n\t_populate_tree()\n\n# ---------------------------\n# Events\n# ---------------------------\nfunc _on_gut_run_started():\n\t_ctrls.run_tests_button.disabled = true\n\t_ctrls.run_selected.visible = false\n\t_ctrls.tabs.visible = false\n\t_ctrls.bg.visible = false\n\t_ctrls.run_tests_button.text = 'Running'\n\tqueue_redraw()\n\n\nfunc _on_gut_run_ended():\n\t_ctrls.run_tests_button.disabled = false\n\t_ctrls.run_selected.visible = true\n\t_ctrls.tabs.visible = true\n\t_ctrls.bg.visible = true\n\t_ctrls.run_tests_button.text = 'Run All'\n\tqueue_redraw()\n\n\nfunc _on_run_tests_pressed():\n\trun_all()\n\n\nfunc _on_run_selected_pressed():\n\trun_selected()\n\n\nfunc _on_tests_item_activated():\n\trun_selected()\n\n# ---------------------------\n# Public\n# ---------------------------\nfunc get_gut():\n\treturn _gut_runner.get_gut()\n\n\nfunc get_config():\n\treturn _config\n\n\nfunc run_all():\n\t_config.options.selected = ''\n\t_config.options.inner_class_name = ''\n\t_config.options.unit_test_name = ''\n\trun_tests()\n\n\nfunc run_tests(options = null):\n\tif(options == null):\n\t\t_config.options = _config_gui.get_options(_config.options)\n\telse:\n\t\t_config.options = options\n\n\t_gut_runner.get_gut().get_test_collector().clear()\n\t_gut_runner.set_gut_config(_config)\n\t_gut_runner.run_tests()\n\n\nfunc run_selected():\n\tvar sel_item = _ctrls.test_tree.get_selected()\n\tif(sel_item == null):\n\t\treturn\n\n\tvar options = _config_gui.get_options(_config.options)\n\tvar meta = sel_item.get_metadata(0)\n\tif(meta.type == TREE_SCRIPT):\n\t\toptions.selected = meta.script.get_file()\n\t\toptions.inner_class_name = meta.inner_class\n\t\toptions.unit_test_name = meta.test\n\telif(meta.type == TREE_DIR):\n\t\toptions.dirs = [meta.path]\n\t\toptions.include_subdirectories = true\n\t\toptions.selected = ''\n\t\toptions.inner_class_name = ''\n\t\toptions.unit_test_name = ''\n\n\trun_tests(options)\n\n\nfunc load_config_file(path):\n\t_config.load_options(path)\n\t_config.options.selected = ''\n\t_config.options.inner_class_name = ''\n\t_config.options.unit_test_name = ''\n\t_config_gui.load_file(path)\n\n\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gui/GutControl.gd.uid",
    "content": "uid://4d6efbk6plxa\n"
  },
  {
    "path": "addons/gut/gui/GutControl.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://4jb53yqktyfg\"]\n\n[ext_resource type=\"Script\" uid=\"uid://4d6efbk6plxa\" path=\"res://addons/gut/gui/GutControl.gd\" id=\"1_eprql\"]\n\n[node name=\"GutControl\" type=\"Control\"]\nlayout_mode = 3\nanchors_preset = 0\noffset_right = 295.0\noffset_bottom = 419.0\nscript = ExtResource(\"1_eprql\")\n\n[node name=\"Bg\" type=\"ColorRect\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\ncolor = Color(0.36, 0.36, 0.36, 1)\n\n[node name=\"VBox\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\n\n[node name=\"Tabs\" type=\"TabContainer\" parent=\"VBox\"]\nlayout_mode = 2\nsize_flags_vertical = 3\ncurrent_tab = 0\n\n[node name=\"Tests\" type=\"Tree\" parent=\"VBox/Tabs\"]\nlayout_mode = 2\nsize_flags_vertical = 3\nhide_root = true\nmetadata/_tab_index = 0\n\n[node name=\"SettingsScroll\" type=\"ScrollContainer\" parent=\"VBox/Tabs\"]\nvisible = false\nlayout_mode = 2\nsize_flags_vertical = 3\nmetadata/_tab_index = 1\n\n[node name=\"Settings\" type=\"VBoxContainer\" parent=\"VBox/Tabs/SettingsScroll\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"Buttons\" type=\"HBoxContainer\" parent=\"VBox\"]\nlayout_mode = 2\n\n[node name=\"RunTests\" type=\"Button\" parent=\"VBox/Buttons\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Run All\"\n\n[node name=\"RunSelected\" type=\"Button\" parent=\"VBox/Buttons\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Run Selected\"\n\n[connection signal=\"item_activated\" from=\"VBox/Tabs/Tests\" to=\".\" method=\"_on_tests_item_activated\"]\n[connection signal=\"pressed\" from=\"VBox/Buttons/RunTests\" to=\".\" method=\"_on_run_tests_pressed\"]\n[connection signal=\"pressed\" from=\"VBox/Buttons/RunSelected\" to=\".\" method=\"_on_run_selected_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/GutRunner.gd",
    "content": "# ##############################################################################\n# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a\n# run of a test suite.\n#\n# This creates its own instance of gut.gd that it manages.  You can set the\n# gut.gd instance if you need to for testing.\n#\n# Set gut_config to an instance of a configured gut_config.gd instance prior to\n# running tests.\n#\n# This will create a GUI and wire it up and apply gut_config.gd options.\n#\n# Running tests:\n# By default, this will run tests once this control has been added to the tree.\n# You can override this by setting ran_from_editor to false before adding\n# this to the tree.  To run tests manually, call run_tests.\n#\n# ##############################################################################\nextends Node2D\n\nconst EXIT_OK = 0\nconst EXIT_ERROR = 1\n\nvar Gut = load('res://addons/gut/gut.gd')\nvar ResultExporter = load('res://addons/gut/result_exporter.gd')\nvar GutConfig = load('res://addons/gut/gut_config.gd')\n\nvar runner_json_path = null\nvar result_bbcode_path = null\nvar result_json_path = null\n\nvar lgr = GutUtils.get_logger()\nvar gut_config = null\n\nvar _hid_gut = null;\n# Lazy loaded gut instance.  Settable for testing purposes.\nvar gut = _hid_gut :\n\tget:\n\t\tif(_hid_gut == null):\n\t\t\t_hid_gut = Gut.new()\n\t\treturn _hid_gut\n\tset(val):\n\t\t_hid_gut = val\nvar _wrote_results = false\n\n# The editor runs this scene using play_custom_scene, which means we cannot\n# pass any info directly to the scene.  Whenever this is being used from\n# somewhere else, you probably want to set this to false before adding this\n# to the tree.\nvar ran_from_editor = true\n\n@onready var _gut_layer = $GutLayer\n@onready var _gui = $GutLayer/GutScene\n\n\nfunc _ready():\n\tGutUtils.WarningsManager.apply_warnings_dictionary(\n\t\tGutUtils.warnings_at_start)\n\tGutUtils.LazyLoader.load_all()\n\n\t# When used from the panel we have to kick off the tests ourselves b/c\n\t# there's no way I know of to interact with the scene that was run via\n\t# play_custom_scene.\n\tif(ran_from_editor):\n\t\t_run_from_editor()\n\n\nfunc _exit_tree():\n\tif(!_wrote_results and ran_from_editor):\n\t\t_write_results_for_gut_panel()\n\n\nfunc _run_from_editor():\n\tvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\n\trunner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path)\n\tresult_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path)\n\tresult_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path)\n\n\tif(gut_config == null):\n\t\tgut_config = GutConfig.new()\n\t\tgut_config.load_options(runner_json_path)\n\n\tcall_deferred('run_tests')\n\n\nfunc _setup_gui(show_gui):\n\tif(show_gui):\n\t\t_gui.gut = gut\n\t\tvar printer = gut.logger.get_printer('gui')\n\t\tprinter.set_textbox(_gui.get_textbox())\n\telse:\n\t\tgut.logger.disable_printer('gui', true)\n\t\t_gui.visible = false\n\n\tvar opts = gut_config.options\n\t_gui.set_font_size(opts.font_size)\n\t_gui.set_font(opts.font_name)\n\tif(opts.font_color != null and opts.font_color.is_valid_html_color()):\n\t\t_gui.set_default_font_color(Color(opts.font_color))\n\tif(opts.background_color != null and opts.background_color.is_valid_html_color()):\n\t\t_gui.set_background_color(Color(opts.background_color))\n\n\t_gui.set_opacity(min(1.0, float(opts.opacity) / 100))\n\t_gui.use_compact_mode(opts.compact_mode)\n\n\nfunc _write_results_for_gut_panel():\n\tvar content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()\n\tvar f = FileAccess.open(result_bbcode_path, FileAccess.WRITE)\n\tif(f != null):\n\t\tf.store_string(content)\n\t\tf = null # closes file\n\telse:\n\t\tpush_error('Could not save bbcode, result = ', FileAccess.get_open_error())\n\n\tvar exporter = ResultExporter.new()\n\t# TODO this should be checked and _wrote_results should maybe not be set, or\n\t# maybe we do not care.  Whichever, it should be clear.\n\tvar _f_result = exporter.write_json_file(gut, result_json_path)\n\t_wrote_results = true\n\n\nfunc _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK):\n\tvar quitting_time = should_exit or \\\n\t\t(should_exit_on_success and gut.get_fail_count() == 0)\n\n\tif(!quitting_time):\n\t\tif(should_exit_on_success):\n\t\t\tlgr.log(\"There are failing tests, exit manually.\")\n\t\t_gui.use_compact_mode(false)\n\t\treturn\n\n\t# For some reason, tests fail asserting that quit was called with 0 if we\n\t# do not do this, but everything is defaulted so I don't know why it gets\n\t# null.\n\tvar exit_code = GutUtils.nvl(override_exit_code, EXIT_OK)\n\n\tif(gut.get_fail_count() > 0):\n\t\texit_code = EXIT_ERROR\n\n\t# Overwrite the exit code with the post_script's exit code if it is set\n\tvar post_hook_inst = gut.get_post_run_script_instance()\n\tif(post_hook_inst != null and post_hook_inst.get_exit_code() != null):\n\t\texit_code = post_hook_inst.get_exit_code()\n\n\tquit(exit_code)\n\n\nfunc _end_run(override_exit_code=EXIT_OK):\n\tif(ran_from_editor):\n\t\t_write_results_for_gut_panel()\n\n\t_handle_quit(gut_config.options.should_exit,\n\t\tgut_config.options.should_exit_on_success,\n\t\toverride_exit_code)\n\n\n# -------------\n# Events\n# -------------\nfunc _on_tests_finished():\n\t_end_run()\n\n\n# -------------\n# Public\n# -------------\nfunc run_tests(show_gui=true):\n\t_setup_gui(show_gui)\n\n\tif(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0):\n\t\tvar err_text = \"You do not have any directories configrued, so GUT doesn't know where to find the tests.  Tell GUT where to find the tests and GUT shall run the tests.\"\n\t\tlgr.error(err_text)\n\t\tpush_error(err_text)\n\t\t_end_run(EXIT_ERROR)\n\t\treturn\n\n\tvar install_check_text = GutUtils.make_install_check_text()\n\tif(install_check_text != GutUtils.INSTALL_OK_TEXT):\n\t\tprint(\"\\n\\n\", GutUtils.version_numbers.get_version_text())\n\t\tlgr.error(install_check_text)\n\t\tpush_error(install_check_text)\n\t\t_end_run(EXIT_ERROR)\n\t\treturn\n\n\tgut.add_children_to = self\n\tif(gut.get_parent() == null):\n\t\tif(gut_config.options.gut_on_top):\n\t\t\t_gut_layer.add_child(gut)\n\t\telse:\n\t\t\tadd_child(gut)\n\n\tgut.end_run.connect(_on_tests_finished)\n\n\tgut_config.apply_options(gut)\n\tvar run_rest_of_scripts = gut_config.options.unit_test_name == ''\n\n\tgut.test_scripts(run_rest_of_scripts)\n\n\nfunc set_gut_config(which):\n\tgut_config = which\n\n\n# for backwards compatibility\nfunc get_gut():\n\treturn gut\n\n\nfunc quit(exit_code):\n\t# Sometimes quitting takes a few seconds.  This gives some indicator\n\t# of what is going on.\n\t_gui.set_title(\"Exiting\")\n\tawait get_tree().process_frame\n\n\tlgr.info(str('Exiting with code ', exit_code))\n\tget_tree().quit(exit_code)\n\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gui/GutRunner.gd.uid",
    "content": "uid://dcxarkn8adlju\n"
  },
  {
    "path": "addons/gut/gui/GutRunner.tscn",
    "content": "[gd_scene load_steps=3 format=3 uid=\"uid://bqy3ikt6vu4b5\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dcxarkn8adlju\" path=\"res://addons/gut/gui/GutRunner.gd\" id=\"1\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://m28heqtswbuq\" path=\"res://addons/gut/GutScene.tscn\" id=\"2_6ruxb\"]\n\n[node name=\"GutRunner\" type=\"Node2D\"]\nscript = ExtResource(\"1\")\n\n[node name=\"GutLayer\" type=\"CanvasLayer\" parent=\".\"]\nlayer = 128\n\n[node name=\"GutScene\" parent=\"GutLayer\" instance=ExtResource(\"2_6ruxb\")]\n"
  },
  {
    "path": "addons/gut/gui/GutSceneTheme.tres",
    "content": "[gd_resource type=\"Theme\" load_steps=2 format=3 uid=\"uid://cstkhwkpajvqu\"]\n\n[ext_resource type=\"FontFile\" uid=\"uid://c6c7gnx36opr0\" path=\"res://addons/gut/fonts/AnonymousPro-Regular.ttf\" id=\"1_df57p\"]\n\n[resource]\ndefault_font = ExtResource(\"1_df57p\")\nLabel/fonts/font = ExtResource(\"1_df57p\")\n"
  },
  {
    "path": "addons/gut/gui/MinGui.tscn",
    "content": "[gd_scene load_steps=5 format=3 uid=\"uid://cnqqdfsn80ise\"]\n\n[ext_resource type=\"Theme\" uid=\"uid://cstkhwkpajvqu\" path=\"res://addons/gut/gui/GutSceneTheme.tres\" id=\"1_farmq\"]\n[ext_resource type=\"FontFile\" uid=\"uid://bnh0lslf4yh87\" path=\"res://addons/gut/fonts/CourierPrime-Regular.ttf\" id=\"2_a2e2l\"]\n[ext_resource type=\"Script\" uid=\"uid://duu660pjff8cv\" path=\"res://addons/gut/gui/gut_gui.gd\" id=\"2_eokrf\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://bvrqqgjpyouse\" path=\"res://addons/gut/gui/ResizeHandle.tscn\" id=\"4_xrhva\"]\n\n[node name=\"Min\" type=\"Panel\"]\nclip_contents = true\ncustom_minimum_size = Vector2(280, 145)\noffset_right = 280.0\noffset_bottom = 145.0\ntheme = ExtResource(\"1_farmq\")\nscript = ExtResource(\"2_eokrf\")\n\n[node name=\"MainBox\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nmetadata/_edit_layout_mode = 1\n\n[node name=\"TitleBar\" type=\"Panel\" parent=\"MainBox\"]\ncustom_minimum_size = Vector2(0, 25)\nlayout_mode = 2\n\n[node name=\"TitleBox\" type=\"HBoxContainer\" parent=\"MainBox/TitleBar\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_top = 2.0\noffset_bottom = 3.0\ngrow_horizontal = 2\ngrow_vertical = 2\nmetadata/_edit_layout_mode = 1\n\n[node name=\"Spacer1\" type=\"CenterContainer\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Title\" type=\"Label\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\ntext = \"Title\"\n\n[node name=\"Spacer2\" type=\"CenterContainer\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"TimeLabel\" type=\"Label\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\ntext = \"0.000s\"\n\n[node name=\"Body\" type=\"HBoxContainer\" parent=\"MainBox\"]\nlayout_mode = 2\nsize_flags_vertical = 3\n\n[node name=\"LeftMargin\" type=\"CenterContainer\" parent=\"MainBox/Body\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"BodyRows\" type=\"VBoxContainer\" parent=\"MainBox/Body\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"ProgressBars\" type=\"HBoxContainer\" parent=\"MainBox/Body/BodyRows\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MainBox/Body/BodyRows/ProgressBars\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label\" type=\"Label\" parent=\"MainBox/Body/BodyRows/ProgressBars/HBoxContainer\"]\nlayout_mode = 2\ntext = \"T:\"\n\n[node name=\"ProgressTest\" type=\"ProgressBar\" parent=\"MainBox/Body/BodyRows/ProgressBars/HBoxContainer\"]\ncustom_minimum_size = Vector2(100, 0)\nlayout_mode = 2\nsize_flags_horizontal = 3\nvalue = 25.0\n\n[node name=\"HBoxContainer2\" type=\"HBoxContainer\" parent=\"MainBox/Body/BodyRows/ProgressBars\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Label\" type=\"Label\" parent=\"MainBox/Body/BodyRows/ProgressBars/HBoxContainer2\"]\nlayout_mode = 2\ntext = \"S:\"\n\n[node name=\"ProgressScript\" type=\"ProgressBar\" parent=\"MainBox/Body/BodyRows/ProgressBars/HBoxContainer2\"]\ncustom_minimum_size = Vector2(100, 0)\nlayout_mode = 2\nsize_flags_horizontal = 3\nvalue = 75.0\n\n[node name=\"PathDisplay\" type=\"VBoxContainer\" parent=\"MainBox/Body/BodyRows\"]\nclip_contents = true\nlayout_mode = 2\nsize_flags_vertical = 3\n\n[node name=\"Path\" type=\"Label\" parent=\"MainBox/Body/BodyRows/PathDisplay\"]\nlayout_mode = 2\ntheme_override_fonts/font = ExtResource(\"2_a2e2l\")\ntheme_override_font_sizes/font_size = 14\ntext = \"res://test/integration/whatever\"\nclip_text = true\ntext_overrun_behavior = 3\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MainBox/Body/BodyRows/PathDisplay\"]\nclip_contents = true\nlayout_mode = 2\n\n[node name=\"S3\" type=\"CenterContainer\" parent=\"MainBox/Body/BodyRows/PathDisplay/HBoxContainer\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"File\" type=\"Label\" parent=\"MainBox/Body/BodyRows/PathDisplay/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\ntheme_override_fonts/font = ExtResource(\"2_a2e2l\")\ntheme_override_font_sizes/font_size = 14\ntext = \"test_this_thing.gd\"\ntext_overrun_behavior = 3\n\n[node name=\"Footer\" type=\"HBoxContainer\" parent=\"MainBox/Body/BodyRows\"]\nlayout_mode = 2\n\n[node name=\"HandleLeft\" parent=\"MainBox/Body/BodyRows/Footer\" node_paths=PackedStringArray(\"resize_control\") instance=ExtResource(\"4_xrhva\")]\nlayout_mode = 2\norientation = 0\nresize_control = NodePath(\"../../../../..\")\nvertical_resize = false\n\n[node name=\"SwitchModes\" type=\"Button\" parent=\"MainBox/Body/BodyRows/Footer\"]\nlayout_mode = 2\ntext = \"Expand\"\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"MainBox/Body/BodyRows/Footer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Continue\" type=\"Button\" parent=\"MainBox/Body/BodyRows/Footer\"]\nlayout_mode = 2\ntext = \"Continue\n\"\n\n[node name=\"HandleRight\" parent=\"MainBox/Body/BodyRows/Footer\" node_paths=PackedStringArray(\"resize_control\") instance=ExtResource(\"4_xrhva\")]\nlayout_mode = 2\nresize_control = NodePath(\"../../../../..\")\nvertical_resize = false\n\n[node name=\"RightMargin\" type=\"CenterContainer\" parent=\"MainBox/Body\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"MainBox\"]\ncustom_minimum_size = Vector2(2.08165e-12, 2)\nlayout_mode = 2\n"
  },
  {
    "path": "addons/gut/gui/NormalGui.tscn",
    "content": "[gd_scene load_steps=5 format=3 uid=\"uid://duxblir3vu8x7\"]\n\n[ext_resource type=\"Theme\" uid=\"uid://cstkhwkpajvqu\" path=\"res://addons/gut/gui/GutSceneTheme.tres\" id=\"1_5hlsm\"]\n[ext_resource type=\"Script\" uid=\"uid://duu660pjff8cv\" path=\"res://addons/gut/gui/gut_gui.gd\" id=\"2_fue6q\"]\n[ext_resource type=\"FontFile\" uid=\"uid://bnh0lslf4yh87\" path=\"res://addons/gut/fonts/CourierPrime-Regular.ttf\" id=\"2_u5uc1\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://bvrqqgjpyouse\" path=\"res://addons/gut/gui/ResizeHandle.tscn\" id=\"4_2r8a8\"]\n\n[node name=\"Large\" type=\"Panel\"]\ncustom_minimum_size = Vector2(500, 150)\noffset_right = 632.0\noffset_bottom = 260.0\ntheme = ExtResource(\"1_5hlsm\")\nscript = ExtResource(\"2_fue6q\")\n\n[node name=\"MainBox\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nmetadata/_edit_layout_mode = 1\n\n[node name=\"TitleBar\" type=\"Panel\" parent=\"MainBox\"]\ncustom_minimum_size = Vector2(0, 25)\nlayout_mode = 2\n\n[node name=\"TitleBox\" type=\"HBoxContainer\" parent=\"MainBox/TitleBar\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_top = 2.0\noffset_bottom = 3.0\ngrow_horizontal = 2\ngrow_vertical = 2\nmetadata/_edit_layout_mode = 1\n\n[node name=\"Spacer1\" type=\"CenterContainer\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"Title\" type=\"Label\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\ntext = \"Title\"\n\n[node name=\"Spacer2\" type=\"CenterContainer\" parent=\"MainBox/TitleBar/TitleBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"TimeLabel\" type=\"Label\" parent=\"MainBox/TitleBar/TitleBox\"]\ncustom_minimum_size = Vector2(90, 0)\nlayout_mode = 2\ntext = \"999.999s\"\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MainBox\"]\nlayout_mode = 2\nsize_flags_vertical = 3\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\"MainBox/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"OutputBG\" type=\"ColorRect\" parent=\"MainBox/HBoxContainer/VBoxContainer\"]\nlayout_mode = 2\nsize_flags_vertical = 3\ncolor = Color(0.0745098, 0.0705882, 0.0784314, 1)\nmetadata/_edit_layout_mode = 1\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/OutputBG\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\n\n[node name=\"S2\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"TestOutput\" type=\"RichTextLabel\" parent=\"MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nfocus_mode = 2\nbbcode_enabled = true\nscroll_following = true\nautowrap_mode = 0\nselection_enabled = true\n\n[node name=\"S1\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"ControlBox\" type=\"HBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"S1\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"ProgressBars\" type=\"VBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\ncustom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"TestBox\" type=\"HBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox\"]\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Tests\"\n\n[node name=\"ProgressTest\" type=\"ProgressBar\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox\"]\ncustom_minimum_size = Vector2(100, 0)\nlayout_mode = 2\nvalue = 25.0\n\n[node name=\"ScriptBox\" type=\"HBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars\"]\nlayout_mode = 2\n\n[node name=\"Label\" type=\"Label\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox\"]\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\nsize_flags_horizontal = 3\ntext = \"Scripts\"\n\n[node name=\"ProgressScript\" type=\"ProgressBar\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox\"]\ncustom_minimum_size = Vector2(100, 0)\nlayout_mode = 2\nvalue = 75.0\n\n[node name=\"PathDisplay\" type=\"VBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"Path\" type=\"Label\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay\"]\nlayout_mode = 2\nsize_flags_vertical = 6\ntheme_override_fonts/font = ExtResource(\"2_u5uc1\")\ntheme_override_font_sizes/font_size = 14\ntext = \"res://test/integration/whatever\"\ntext_overrun_behavior = 3\n\n[node name=\"HBoxContainer\" type=\"HBoxContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay\"]\nlayout_mode = 2\nsize_flags_vertical = 3\n\n[node name=\"S3\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"File\" type=\"Label\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\ntheme_override_fonts/font = ExtResource(\"2_u5uc1\")\ntheme_override_font_sizes/font_size = 14\ntext = \"test_this_thing.gd\"\ntext_overrun_behavior = 3\n\n[node name=\"Spacer1\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\nvisible = false\nlayout_mode = 2\nsize_flags_horizontal = 10\n\n[node name=\"Continue\" type=\"Button\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\nlayout_mode = 2\nsize_flags_vertical = 4\ntext = \"Continue\n\"\n\n[node name=\"S3\" type=\"CenterContainer\" parent=\"MainBox/HBoxContainer/VBoxContainer/ControlBox\"]\ncustom_minimum_size = Vector2(5, 0)\nlayout_mode = 2\n\n[node name=\"BottomPad\" type=\"CenterContainer\" parent=\"MainBox\"]\ncustom_minimum_size = Vector2(0, 5)\nlayout_mode = 2\n\n[node name=\"Footer\" type=\"HBoxContainer\" parent=\"MainBox\"]\nlayout_mode = 2\n\n[node name=\"SidePad1\" type=\"CenterContainer\" parent=\"MainBox/Footer\"]\ncustom_minimum_size = Vector2(2, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"ResizeHandle3\" parent=\"MainBox/Footer\" node_paths=PackedStringArray(\"resize_control\") instance=ExtResource(\"4_2r8a8\")]\ncustom_minimum_size = Vector2(25, 25)\nlayout_mode = 2\norientation = 0\nresize_control = NodePath(\"../../..\")\n\n[node name=\"SwitchModes\" type=\"Button\" parent=\"MainBox/Footer\"]\nlayout_mode = 2\ntext = \"Compact\n\"\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"MainBox/Footer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"ResizeHandle2\" parent=\"MainBox/Footer\" node_paths=PackedStringArray(\"resize_control\") instance=ExtResource(\"4_2r8a8\")]\ncustom_minimum_size = Vector2(25, 25)\nlayout_mode = 2\nresize_control = NodePath(\"../../..\")\n\n[node name=\"SidePad2\" type=\"CenterContainer\" parent=\"MainBox/Footer\"]\ncustom_minimum_size = Vector2(2, 2.08165e-12)\nlayout_mode = 2\n\n[node name=\"BottomPad2\" type=\"CenterContainer\" parent=\"MainBox\"]\ncustom_minimum_size = Vector2(2.08165e-12, 2)\nlayout_mode = 2\n"
  },
  {
    "path": "addons/gut/gui/OutputText.gd",
    "content": "@tool\nextends VBoxContainer\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar PanelControls = load('res://addons/gut/gui/panel_controls.gd')\n\n# ##############################################################################\n# Keeps search results from the TextEdit\n# ##############################################################################\nclass TextEditSearcher:\n\tvar te : TextEdit\n\tvar _last_term = ''\n\tvar _last_pos = Vector2(-1, -1)\n\tvar _ignore_caret_change = false\n\n\tfunc set_text_edit(which):\n\t\tte = which\n\t\tte.caret_changed.connect(_on_caret_changed)\n\n\n\tfunc _on_caret_changed():\n\t\tif(_ignore_caret_change):\n\t\t\t_ignore_caret_change = false\n\t\telse:\n\t\t\t_last_pos = _get_caret();\n\n\n\tfunc _get_caret():\n\t\treturn Vector2(te.get_caret_column(), te.get_caret_line())\n\n\n\tfunc _set_caret_and_sel(pos, len):\n\t\tte.set_caret_line(pos.y)\n\t\tte.set_caret_column(pos.x)\n\t\tif(len > 0):\n\t\t\tte.select(pos.y, pos.x, pos.y, pos.x + len)\n\n\n\tfunc _find(term, search_flags):\n\t\tvar pos = _get_caret()\n\t\tif(term == _last_term):\n\t\t\tif(search_flags == 0):\n\t\t\t\tpos = _last_pos\n\t\t\t\tpos.x += 1\n\t\t\telse:\n\t\t\t\tpos = _last_pos\n\t\t\t\tpos.x -= 1\n\n\t\tvar result = te.search(term, search_flags, pos.y, pos.x)\n#\t\tprint('searching from ', pos, ' for \"', term, '\" = ', result)\n\t\tif(result.y != -1):\n\t\t\t_ignore_caret_change = true\n\t\t\t_set_caret_and_sel(result, term.length())\n\t\t\t_last_pos = result\n\n\t\t_last_term = term\n\n\tfunc find_next(term):\n\t\t_find(term, 0)\n\n\tfunc find_prev(term):\n\t\t_find(term, te.SEARCH_BACKWARDS)\n\n\n# ##############################################################################\n# Start OutputText control code\n# ##############################################################################\n@onready var _ctrls = {\n\toutput = $Output,\n\tsettings_bar = $Settings,\n\tuse_colors = $Settings/UseColors,\n\tword_wrap = $Settings/WordWrap,\n\n\tcopy_button = $Toolbar/CopyButton,\n\tclear_button = $Toolbar/ClearButton,\n\tshow_search = $Toolbar/ShowSearch,\n\tcaret_position = $Toolbar/LblPosition,\n\n\tsearch_bar = {\n\t\tbar = $Search,\n\t\tsearch_term = $Search/SearchTerm,\n\t}\n}\n\nvar _sr = TextEditSearcher.new()\nvar _highlighter : CodeHighlighter\nvar _font_name = null\nvar _user_prefs = GutEditorGlobals.user_prefs\nvar _font_name_pctrl = null\nvar _font_size_pctrl = null\n\n# Automatically used when running the OutputText scene from the editor.  Changes\n# to this method only affect test-running the control through the editor.\nfunc _test_running_setup():\n\t_ctrls.use_colors.text = 'use colors'\n\t_ctrls.show_search.text = 'search'\n\t_ctrls.word_wrap.text = 'ww'\n\n\tset_all_fonts(\"CourierPrime\")\n\tset_font_size(30)\n\n\t_ctrls.output.queue_redraw()\n\tload_file('user://.gut_editor.bbcode')\n\tawait get_tree().process_frame\n\n\tshow_search(true)\n\t_ctrls.output.set_caret_line(0)\n\t_ctrls.output.scroll_vertical = 0\n\t_ctrls.output.caret_changed.connect(_on_caret_changed)\n\n\nfunc _ready():\n\t_sr.set_text_edit(_ctrls.output)\n\t_ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons')\n\t_ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons')\n\t_ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons')\n\n\t_setup_colors()\n\t_ctrls.use_colors.button_pressed = true\n\t_use_highlighting(true)\n\n\tif(get_parent() == get_tree().root):\n\t\t_test_running_setup()\n\n\t_ctrls.settings_bar.visible = false\n\t_add_other_ctrls()\n\n\nfunc _add_other_ctrls():\n\tvar fname = 'CourierNew'\n\tif(_user_prefs != null):\n\t\tfname = _user_prefs.output_font_name.value\n\t_font_name_pctrl = PanelControls.SelectControl.new('Font', fname, GutUtils.avail_fonts,\n\t\t\"The font, you know, for the text below.  Change it, see what it does.\")\n\t_font_name_pctrl.changed.connect(_on_font_name_changed)\n\t_font_name_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN\n\t_ctrls.settings_bar.add_child(_font_name_pctrl)\n\tset_all_fonts(fname)\n\n\tvar fsize = 30\n\tif(_user_prefs != null):\n\t\tfsize = _user_prefs.output_font_size.value\n\t_font_size_pctrl = PanelControls.NumberControl.new('Font Size', fsize , 5, 100,\n\t\t\"The size of 'The Font'.\")\n\t_font_size_pctrl.changed.connect(_on_font_size_changed)\n\t_font_size_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN\n\t_ctrls.settings_bar.add_child(_font_size_pctrl)\n\tset_font_size(fsize)\n\n\n# ------------------\n# Private\n# ------------------\n\n# Call this after changes in colors and the like to get them to apply.  reloads\n# the text of the output control.\nfunc _refresh_output():\n\tvar orig_pos = _ctrls.output.scroll_vertical\n\tvar text = _ctrls.output.text\n\n\t_ctrls.output.text = text\n\t_ctrls.output.scroll_vertical = orig_pos\n\n\nfunc _create_highlighter(default_color=Color(1, 1, 1, 1)):\n\tvar to_return = CodeHighlighter.new()\n\n\tto_return.function_color = default_color\n\tto_return.number_color = default_color\n\tto_return.symbol_color = default_color\n\tto_return.member_variable_color = default_color\n\n\tvar keywords = [\n\t\t['Failed', Color.RED],\n\t\t['Passed', Color.GREEN],\n\t\t['Pending', Color.YELLOW],\n\t\t['Orphans', Color.YELLOW],\n\t\t['WARNING', Color.YELLOW],\n\t\t['ERROR', Color.RED]\n\t]\n\n\tfor keyword in keywords:\n\t\tto_return.add_keyword_color(keyword[0], keyword[1])\n\n\treturn to_return\n\n\nfunc _setup_colors():\n\t_ctrls.output.clear()\n\n\tvar f_color = null\n\tif (_ctrls.output.theme == null) :\n\t\tf_color = get_theme_color(\"font_color\")\n\telse :\n\t\tf_color = _ctrls.output.theme.font_color\n\n\t_highlighter = _create_highlighter()\n\t_ctrls.output.queue_redraw()\n\n\n\nfunc _use_highlighting(should):\n\tif(should):\n\t\t_ctrls.output.syntax_highlighter = _highlighter\n\telse:\n\t\t_ctrls.output.syntax_highlighter = null\n\t_refresh_output()\n\n# ------------------\n# Events\n# ------------------\nfunc _on_caret_changed():\n\tvar txt = str(\"line:\",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column())\n\t_ctrls.caret_position.text = str(txt)\n\nfunc _on_font_size_changed():\n\tset_font_size(_font_size_pctrl.value)\n\tif(_user_prefs != null):\n\t\t_user_prefs.output_font_size.value = _font_size_pctrl.value\n\t\t_user_prefs.output_font_size.save_it()\n\nfunc _on_font_name_changed():\n\tset_all_fonts(_font_name_pctrl.text)\n\tif(_user_prefs != null):\n\t\t_user_prefs.output_font_name.value = _font_name_pctrl.text\n\t\t_user_prefs.output_font_name.save_it()\n\nfunc _on_CopyButton_pressed():\n\tcopy_to_clipboard()\n\nfunc _on_UseColors_pressed():\n\t_use_highlighting(_ctrls.use_colors.button_pressed)\n\nfunc _on_ClearButton_pressed():\n\tclear()\n\nfunc _on_ShowSearch_pressed():\n\tshow_search(_ctrls.show_search.button_pressed)\n\nfunc _on_SearchTerm_focus_entered():\n\t_ctrls.search_bar.search_term.call_deferred('select_all')\n\nfunc _on_SearchNext_pressed():\n\t_sr.find_next(_ctrls.search_bar.search_term.text)\n\nfunc _on_SearchPrev_pressed():\n\t_sr.find_prev(_ctrls.search_bar.search_term.text)\n\nfunc _on_SearchTerm_text_changed(new_text):\n\tif(new_text == ''):\n\t\t_ctrls.output.deselect()\n\telse:\n\t\t_sr.find_next(new_text)\n\nfunc _on_SearchTerm_text_entered(new_text):\n\tif(Input.is_physical_key_pressed(KEY_SHIFT)):\n\t\t_sr.find_prev(new_text)\n\telse:\n\t\t_sr.find_next(new_text)\n\nfunc _on_SearchTerm_gui_input(event):\n\tif(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE):\n\t\tshow_search(false)\n\nfunc _on_WordWrap_pressed():\n\tif(_ctrls.word_wrap.button_pressed):\n\t\t_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY\n\telse:\n\t\t_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE\n\n\t_ctrls.output.queue_redraw()\n\nfunc _on_settings_pressed():\n\t_ctrls.settings_bar.visible = $Toolbar/ShowSettings.button_pressed\n\n# ------------------\n# Public\n# ------------------\nfunc show_search(should):\n\t_ctrls.search_bar.bar.visible = should\n\tif(should):\n\t\t_ctrls.search_bar.search_term.grab_focus()\n\t\t_ctrls.search_bar.search_term.select_all()\n\t_ctrls.show_search.button_pressed = should\n\n\nfunc search(text, start_pos, highlight=true):\n\treturn _sr.find_next(text)\n\n\nfunc copy_to_clipboard():\n\tvar selected = _ctrls.output.get_selected_text()\n\tif(selected != ''):\n\t\tDisplayServer.clipboard_set(selected)\n\telse:\n\t\tDisplayServer.clipboard_set(_ctrls.output.text)\n\n\nfunc clear():\n\t_ctrls.output.text = ''\n\n\nfunc _set_font(font_name, custom_name):\n\tvar rtl = _ctrls.output\n\tif(font_name == null):\n\t\trtl.remove_theme_font_override(custom_name)\n\telse:\n\t\tvar dyn_font = FontFile.new()\n\t\tdyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')\n\t\trtl.add_theme_font_override(custom_name, dyn_font)\n\n\nfunc set_all_fonts(base_name):\n\t_font_name = GutUtils.nvl(base_name, 'Default')\n\n\tif(base_name == 'Default'):\n\t\t_set_font(null, 'font')\n\t\t_set_font(null, 'normal_font')\n\t\t_set_font(null, 'bold_font')\n\t\t_set_font(null, 'italics_font')\n\t\t_set_font(null, 'bold_italics_font')\n\telse:\n\t\t_set_font(base_name + '-Regular', 'font')\n\t\t_set_font(base_name + '-Regular', 'normal_font')\n\t\t_set_font(base_name + '-Bold', 'bold_font')\n\t\t_set_font(base_name + '-Italic', 'italics_font')\n\t\t_set_font(base_name + '-BoldItalic', 'bold_italics_font')\n\n\nfunc set_font_size(new_size):\n\t_ctrls.output.set(\"theme_override_font_sizes/font_size\", new_size)\n\n\nfunc set_use_colors(value):\n\tpass\n\n\nfunc get_use_colors():\n\treturn false;\n\n\nfunc get_rich_text_edit():\n\treturn _ctrls.output\n\n\nfunc load_file(path):\n\tvar f = FileAccess.open(path, FileAccess.READ)\n\tif(f == null):\n\t\treturn\n\n\tvar t = f.get_as_text()\n\tf = null # closes file\n\t_ctrls.output.text = t\n\t_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()\n\t_ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count())\n\n\nfunc add_text(text):\n\tif(is_inside_tree()):\n\t\t_ctrls.output.text += text\n\n\nfunc scroll_to_line(line):\n\t_ctrls.output.scroll_vertical = line\n\t_ctrls.output.set_caret_line(line)\n"
  },
  {
    "path": "addons/gut/gui/OutputText.gd.uid",
    "content": "uid://e7xrdfvw3g7c\n"
  },
  {
    "path": "addons/gut/gui/OutputText.tscn",
    "content": "[gd_scene load_steps=6 format=4 uid=\"uid://bqmo4dj64c7yl\"]\n\n[ext_resource type=\"Script\" uid=\"uid://e7xrdfvw3g7c\" path=\"res://addons/gut/gui/OutputText.gd\" id=\"1\"]\n\n[sub_resource type=\"Image\" id=\"Image_p7oqn\"]\ndata = {\n\"data\": PackedByteArray(\"/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9cXAD/XFwA/1xcAP9cXAD/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XFx//1xcAP9cXAD/XFwA/1xcAP9cXAD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11d//9cXH//XFwA/1xcAP9cXAD/XFwA/11dAP9dXQD/XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXf//XV3//1xcf/9cXAD/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XV3//11d//9dXf//XFx//1xcAP9cXAD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11dAP9dXQD/XV0A/1xcAP9cXAD/XFwA/11dAP9dXQD/XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11dAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXQD/XV0A/11dAP9dXef/Wlo2/15eOf9dXen/XV3//11d//9dXef/Wlo2/15eOf9dXen/XV3//11d//9dXQD/XV0A/1tbAP9bWwD/W1sq/1paAP9eXgD/W1sq/11d6f9cXOj/XV0p/1paAP9eXgD/W1sq/11d6f9cXOj/XFwA/1xcAP9bWwD/W1sA/1tbAP9bWy3/XV0s/1tbAP9bWyr/W1sq/11dAP9bWy3/XV0s/1tbAP9bWyr/W1sq/1tbAP9bWwD/W1sA/1tbAP9bWy3/XFzr/1xc6v9ZWSv/W1sA/1tbAP9bWy3/XFzr/1xc6v9ZWSv/W1sA/1tbAP9bWwD/W1sA/1xcAP9cXAD/XFzr/11d//9dXf//XV3p/1tbO/9cXD3/XFzr/11d//9dXf//XV3p/1tbO/9cXD3/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXQD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAA==\"),\n\"format\": \"RGBA8\",\n\"height\": 16,\n\"mipmaps\": false,\n\"width\": 16\n}\n\n[sub_resource type=\"ImageTexture\" id=\"ImageTexture_srqj5\"]\nimage = SubResource(\"Image_p7oqn\")\n\n[sub_resource type=\"FontFile\" id=\"FontFile_lygvu\"]\ndata = PackedByteArray(\"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BMwAAATMAekEzAD/BMwAPATMALMEzAAyBMwAkwTMAeUEzAEpBMwBawTMAL8EzACqBMwBdATMAKoEzAHfBMwAxgTMALwEzADcBMwAuwTMAL8EzACTBMwAvATMAMkEzADlBMwAzgTMAMAEzAHfBMwBiATMAH0EzACqBMwArwTMAP4EzAAyBMwACgTMAFAEzAB2BMwAUATMAFAEzABQBMwAYgTMAFAEzADIBMwAcwTMAFAEzABQBMwAHgTMAEYEzABYBMwAUATMAFgEzABQBMwAngTMAHoEzABGBMwACgTM//YEzAAyBMwAPATMAKcEzAFKBMwAxgTMAWYEzADoBMz/5ATMATkEzACHBMwAJQTMAH8EzAB/BMwAkgTMALsEzAB/BMwATQTMALsEzAC+BMwAQwTMALEEzP/8BMwATQTMAIQEzAAlBMwAfwTMAJMEzACzBMwAdQTMAEgEzAAyBMz/9gTMAEYEzAAyBMwAsgTMALEEzAIbBMwA7gTMAJYEzAHpBMwAfwTMAMsEzABvBMwAPATMAhsEzACzBMwBNQTMACQEzADyBMwAjATMAIQEzAEiBMwA9gTMAKoEzAEgBMwBLgTMAaEEzADaBMwAgwTMAd8EzAFfBMwBNATMAOsEzP/7BMz/+wTMABQEzAEGBMwACgTMAAoEzAAKBMwACgTMAAoEzAAKBMwAAATMAHYEzABQBMwAUATMAFAEzABQBMwAyATMAMgEzADIBMwAyATMADIEzABGBMwAWATMAFgEzABYBMwAWATMAFgEzADpBMwAWATMAEYEzABGBMwARgTMAEYEzAA8BMwAbgTMACgEzACHBMwAhwTMAIcEzACHBMwAhwTMAIcEzABLBMwAfwTMAJIEzACSBMwAkgTMAJIEzAC7BMwAuwTMALsEzAC7BMwAjATMAE0EzACEBMwAhATMAIQEzACEBMwAhATMAKoEzABUBMwASATMAEgEzABIBMwASATMADIEzAAlBMwAMgTMAAoEzACHBMwACgTMAIcEzAAKBMwAhwTMAHYEzAB/BMwAdgTMAH8EzAB2BMwAfwTMAHYEzAB/BMwAUATMAH8EzABQBMwAlQTMAFAEzACSBMwAUATMAJIEzABQBMwAkgTMAFAEzACSBMwAYgTMAH8EzABiBMwAfwTMAGIEzAB/BMwAYgTMAH8EzABQBMwATQTMAB4EzAAU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AS4AAwABBAkABQAaAVgAAwABBAkABgAoAXIAAwABBAkACAAkAZoAAwABBAkACQBKAb4AAwABBAkACwA2AggAAwABBAkADAAsAj4AAwABBAkADQEgAmoAAwABBAkADgA0A4oAQwBvAHAAeQByAGkAZwBoAHQAIAAyADAAMQA1ACAAVABoAGUAIABDAG8AdQByAGkAZQByACAAUAByAGkAbQBlACAAUAByAG8AagBlAGMAdAAgAEEAdQB0AGgAbwByAHMAIAAoAGgAdAB0AHAAcwA6AC8ALwBnAGkAdABoAHUAYgAuAGMAbwBtAC8AcQB1AG8AdABlAHUAbgBxAHUAbwB0AGUAYQBwAHAAcwAvAEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlACkALgBDAG8AdQByAGkAZQByACAAUAByAGkAbQBlAFIAZQBnAHUAbABhAHIAMwAuADAAMQA4ADsAUQBVAFEAQQA7AEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlAC0AUgBlAGcAdQBsAGEAcgBDAG8AdQByAGkAZQByACAAUAByAGkAbQBlACAAUgBlAGcAdQBsAGEAcgBWAGUAcgBzAGkAbwBuACAAMwAuADAAMQA4AEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlAC0AUgBlAGcAdQBsAGEAcgBRAHUAbwB0AGUALQBVAG4AcQB1AG8AdABlACAAQQBwAHAAcwBBAGwAYQBuACAARABhAGcAdQBlAC0ARwByAGUAZQBuAGUALAAgAFEAdQBvAHQAZQAtAFUAbgBxAHUAbwB0AGUAIABBAHAAcABzAGgAdAB0AHAAOgAvAC8AcQB1AG8AdABlAHUAbgBxAHUAbwB0AGUAYQBwAHAAcwAuAGMAbwBtAGgAdAB0AHAAOgAvAC8AYgBhAHMAaQBjAHIAZQBjAGkAcABlAC4AYwBvAG0AVABoAGkAcwAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABpAHMAIABsAGkAYwBlAG4AcwBlAGQAIAB1AG4AZABlAHIAIAB0AGgAZQAgAFMASQBMACAATwBwAGUAbgAgAEYAbwBuAHQAIABMAGkAYwBlAG4AcwBlACwAIABWAGUAcgBzAGkAbwBuACAAMQAuADEALgAgAFQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAaQBzACAAYQB2AGEAaQBsAGEAYgBsAGUAIAB3AGkAdABoACAAYQAgAEYAQQBRACAAYQB0ADoAIABoAHQAdABwADoALwAvAHMAYwByAGkAcAB0AHMALgBzAGkAbAAuAG8AcgBnAC8ATwBGAEwAaAB0AHQAcAA6AC8ALwBzAGMAcgBpAHAAdABzAC4AcwBpAGwALgBvAHIAZwAvAE8ARgBMAAIAAAAAAAD/RwCCAAAAAQAAAAAAAAAAAAAAAAAAAAABkAAAAQIAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkAGgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAowCEAIUAvQCWAOgAhgCOAIsAnQCkAIoA2gCDAJMBAwEEAI0BBQCIAMMA3gEGAJ4A9QD0APYAogCtAMkAxwCuAGIAYwCQAGQAywBlAMgAygDPAMwAzQDOAOkAZgDTANAA0QCvAGcA8ACRANYA1ADVAGgA6wDtAIkAagBpAGsAbQBsAG4AoABvAHEAcAByAHMAdQB0AHYAdwDqAHgAegB5AHsAfQB8ALgAoQB/AH4AgACBAOwA7gC6AQcBCAEJAQoBCwEMAP0A/gENAQ4BDwEQAP8BAAERARIBEwEBARQBFQEWARcBGAEZARoBGwEcAR0A+AD5AR4BHwEgASEBIgEjASQBJQEmAScBKAEpAPoBKgErASwBLQEuAS8BMAExATIBMwE0ATUA4gDjATYBNwE4ATkBOgE7ATwBPQE+AT8AsACxAUABQQFCAUMBRAFFAUYBRwFIAUkA+wD8AOQA5QFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcAuwFYAVkBWgFbAOYA5wCmAVwBXQFeANgA4QDbANwA3QDgANkA3wCbALIAswC2ALcAxAC0ALUAxQCCAMIAhwCrAMYAvgC/ALwAjAFfAJgBYACaAJkA7wClAJIAnACnAI8AlACVALkAwADBAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcQDXAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEAqQCqAYIBgwD3AYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYBE5VTEwHdW5pMDBCMgd1bmkwMEIzB3VuaTAzQkMHdW5pMDBCOQdBbWFjcm9uB2FtYWNyb24GQWJyZXZlBmFicmV2ZQdBb2dvbmVrB2FvZ29uZWsLQ2NpcmN1bWZsZXgLY2NpcmN1bWZsZXgKQ2RvdGFjY2VudApjZG90YWNjZW50BkRjYXJvbgZkY2Fyb24GRGNyb2F0B0VtYWNyb24HZW1hY3JvbgpFZG90YWNjZW50CmVkb3RhY2NlbnQHRW9nb25lawdlb2dvbmVrBkVjYXJvbgZlY2Fyb24LR2NpcmN1bWZsZXgLZ2NpcmN1bWZsZXgKR2RvdGFjY2VudApnZG90YWNjZW50B3VuaTAxMjIHdW5pMDEyMwtIY2lyY3VtZmxleAtoY2lyY3VtZmxleARIYmFyBGhiYXIHSW1hY3JvbgdpbWFjcm9uB0lvZ29uZWsHaW9nb25lawJJSgJpagtKY2lyY3VtZmxleAtqY2lyY3VtZmxleAd1bmkwMTM2B3VuaTAxMzcGTGFjdXRlBmxhY3V0ZQd1bmkwMTNCB3VuaTAxM0MGTGNhcm9uBmxjYXJvbgZOYWN1dGUGbmFjdXRlB3VuaTAxNDUHdW5pMDE0NgZOY2Fyb24GbmNhcm9uB09tYWNyb24Hb21hY3Jvbg1PaHVuZ2FydW1sYXV0DW9odW5nYXJ1bWxhdXQGUmFjdXRlBnJhY3V0ZQd1bmkwMTU2B3VuaTAxNTcGUmNhcm9uBnJjYXJvbgZTYWN1dGUGc2FjdXRlC1NjaXJjdW1mbGV4C3NjaXJjdW1mbGV4B3VuaTAyMUEHdW5pMDIxQgZUY2Fyb24GdGNhcm9uB1VtYWNyb24HdW1hY3JvbgZVYnJldmUGdWJyZXZlBVVyaW5nBXVyaW5nDVVodW5nYXJ1bWxhdXQNdWh1bmdhcnVtbGF1dAdVb2dvbmVrB3VvZ29uZWsGWmFjdXRlBnphY3V0ZQpaZG90YWNjZW50Cnpkb3RhY2NlbnQHdW5pMDIxOAd1bmkwMjE5B3VuaTAyMzcHdW5pMDNBOQd1bmkwMzk0B3VuaTAxNjIHdW5pMDE2MwZFYnJldmUGSWJyZXZlBkl0aWxkZQRMZG90A0VuZwZPYnJldmUEVGJhcgZVdGlsZGUGV2FjdXRlC1djaXJjdW1mbGV4CVdkaWVyZXNpcwZXZ3JhdmULWWNpcmN1bWZsZXgGWWdyYXZlBmVicmV2ZQZpYnJldmUGaXRpbGRlBGxkb3QDZW5nBm9icmV2ZQR0YmFyBnV0aWxkZQZ3YWN1dGULd2NpcmN1bWZsZXgJd2RpZXJlc2lzBndncmF2ZQt5Y2lyY3VtZmxleAZ5Z3JhdmUHdW5pMjA3NAd1bmkwMEFEB3VuaTIwMTEHdW5pMDBBMARFdXJvB3VuaTIwQTkHdW5pMjIxNQd1bmkwMEI1Bm1pbnV0ZQZzZWNvbmQHdW5pMDMyNgx1bmkwMzI2LmNhc2UKYWN1dGUuY2FzZQpicmV2ZS5jYXNlCWNhcm9uLmFsdApjYXJvbi5jYXNlDGNlZGlsbGEuY2FzZQ9jaXJjdW1mbGV4LmNhc2UNZGllcmVzaXMuY2FzZQ5kb3RhY2NlbnQuY2FzZQpncmF2ZS5jYXNlEWh1bmdhcnVtbGF1dC5jYXNlC21hY3Jvbi5jYXNlC29nb25lay5jYXNlCXJpbmcuY2FzZQp0aWxkZS5jYXNlAAABAAH//wAPAAEAAAAMAAAAHAAAAAIAAgFSAVMAAgGAAYEAAwAIAAIAEAAYAAEAAgFSAVMAAQAEAAECAQABAAQAAQIrAAEAAAAKAKIBmgADREZMVAAUZ3JlawAobGF0bgA8AAQAAAAA//8ABQAAAAYADAAVABsABAAAAAD//wAFAAEABwANABYAHAAWAANDQVQgACZNT0wgADhST00gAEoAAP//AAUAAgAIAA4AFwAdAAD//wAGAAMACQAPABIAGAAeAAD//wAGAAQACgAQABMAGQAfAAD//wAGAAUACwARABQAGgAgACFhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhjYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5mcmFjANRmcmFjANRmcmFjANRmcmFjANRmcmFjANRmcmFjANRsb2NsANpsb2NsAOBsb2NsAOZvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPIAAAABAAAAAAABAAcAAAABAAUAAAABAAMAAAABAAIAAAABAAEAAAABAAYAAAABAAQACgAWAJAAkACyAPYBFgFSAZoB5AISAAEAAAABAAgAAgA6ABoAeABxAHIBcwBrAHkBigBrAHkBiAGMAYIBhgEnASgBhwGFAYMBiQGOAY0BjwGLAREBEgGBAAEAGgAUABUAFgAXACQAMgBDAEQAUgBpAG4AcwB3AQ0BDgEqASsBLAEtAS4BLwEwATEBVAFVAYAAAQAAAAEACAACAA4ABAEnASgBEQESAAEABAENAQ4BVAFVAAYAAAACAAoAJAADAAAAAgAUAC4AAQAUAAEAAAAIAAEAAQBPAAMAAAACABoAFAABABoAAQAAAAgAAQABAHYAAQABAC8AAQAAAAEACAACAA4ABAB4AHEAcgFzAAIAAQAUABcAAAAEAAAAAQAIAAEALAACAAoAIAACAAYADgB7AAMAEgAVAHoAAwASABcAAQAEAHwAAwASABcAAQACABQAFgAGAAAAAgAKACQAAwABACwAAQASAAAAAQAAAAkAAQACACQARAADAAEAEgABABwAAAABAAAACQACAAEAEwAcAAAAAQACADIAUgABAAAAAQAIAAIAIgAOAYoBiAGMAYIBhgGHAYUBgwGJAY4BjQGPAYsBgQABAA4AQwBpAG4AcwB3ASoBKwEsAS0BLgEvATABMQGAAAQAAAABAAgAAQAeAAIACgAUAAEABAFZAAIAdgABAAQBaAACAHYAAQACAC8ATwABAAAAAQAIAAIADgAEAGsAeQBrAHkAAQAEACQAMgBEAFIAAA==\")\nfont_name = \"Courier Prime\"\nstyle_name = \"Regular\"\nfont_style = 4\nsubpixel_positioning = 0\nmsdf_pixel_range = 14\nmsdf_size = 128\ncache/0/25/0/ascent = 20.0\ncache/0/25/0/descent = 9.0\ncache/0/25/0/underline_position = 3.04688\ncache/0/25/0/underline_thickness = 1.59375\ncache/0/25/0/scale = 1.0\ncache/0/25/0/glyphs/3/advance = Vector2(15, 28)\ncache/0/25/0/glyphs/3/offset = Vector2(0, 0)\ncache/0/25/0/glyphs/3/size = Vector2(0, 0)\ncache/0/25/0/glyphs/3/uv_rect = Rect2(0, 0, 0, 0)\ncache/0/25/0/glyphs/3/texture_idx = -1\ncache/0/30/0/ascent = 24.0\ncache/0/30/0/descent = 11.0\ncache/0/30/0/underline_position = 3.65625\ncache/0/30/0/underline_thickness = 1.90625\ncache/0/30/0/scale = 1.0\ncache/0/30/0/glyphs/3/advance = Vector2(18, 34)\ncache/0/30/0/glyphs/3/offset = Vector2(0, 0)\ncache/0/30/0/glyphs/3/size = Vector2(0, 0)\ncache/0/30/0/glyphs/3/uv_rect = Rect2(0, 0, 0, 0)\ncache/0/30/0/glyphs/3/texture_idx = -1\ncache/0/16/0/ascent = 13.0\ncache/0/16/0/descent = 6.0\ncache/0/16/0/underline_position = 1.95313\ncache/0/16/0/underline_thickness = 1.01563\ncache/0/16/0/scale = 1.0\n\n[sub_resource type=\"CodeHighlighter\" id=\"CodeHighlighter_b4xqv\"]\nnumber_color = Color(1, 1, 1, 1)\nsymbol_color = Color(1, 1, 1, 1)\nfunction_color = Color(1, 1, 1, 1)\nmember_variable_color = Color(1, 1, 1, 1)\nkeyword_colors = {\n\"ERROR\": Color(1, 0, 0, 1),\n\"Failed\": Color(1, 0, 0, 1),\n\"Orphans\": Color(1, 1, 0, 1),\n\"Passed\": Color(0, 1, 0, 1),\n\"Pending\": Color(1, 1, 0, 1),\n\"WARNING\": Color(1, 1, 0, 1)\n}\n\n[node name=\"OutputText\" type=\"VBoxContainer\"]\noffset_right = 862.0\noffset_bottom = 523.0\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nscript = ExtResource(\"1\")\n\n[node name=\"Toolbar\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"ShowSearch\" type=\"Button\" parent=\"Toolbar\"]\nlayout_mode = 2\ntooltip_text = \"Search\"\ntoggle_mode = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"ShowSettings\" type=\"Button\" parent=\"Toolbar\"]\nlayout_mode = 2\ntooltip_text = \"Settings\"\ntoggle_mode = true\ntext = \"...\"\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"Toolbar\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"LblPosition\" type=\"Label\" parent=\"Toolbar\"]\nlayout_mode = 2\n\n[node name=\"CopyButton\" type=\"Button\" parent=\"Toolbar\"]\nlayout_mode = 2\ntext = \" Copy \"\n\n[node name=\"ClearButton\" type=\"Button\" parent=\"Toolbar\"]\nlayout_mode = 2\ntext = \"  Clear  \"\n\n[node name=\"Settings\" type=\"HBoxContainer\" parent=\".\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"WordWrap\" type=\"Button\" parent=\"Settings\"]\nlayout_mode = 2\ntooltip_text = \"Word Wrap\"\ntoggle_mode = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"UseColors\" type=\"Button\" parent=\"Settings\"]\nlayout_mode = 2\ntooltip_text = \"Colorized Text\"\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Output\" type=\"TextEdit\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\ntheme_override_fonts/font = SubResource(\"FontFile_lygvu\")\ntheme_override_font_sizes/font_size = 30\ndeselect_on_focus_loss_enabled = false\nvirtual_keyboard_enabled = false\nmiddle_mouse_paste_enabled = false\nscroll_smooth = true\nsyntax_highlighter = SubResource(\"CodeHighlighter_b4xqv\")\nhighlight_all_occurrences = true\nhighlight_current_line = true\n\n[node name=\"Search\" type=\"HBoxContainer\" parent=\".\"]\nvisible = false\nlayout_mode = 2\n\n[node name=\"SearchTerm\" type=\"LineEdit\" parent=\"Search\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\n\n[node name=\"SearchNext\" type=\"Button\" parent=\"Search\"]\nlayout_mode = 2\ntext = \"Next\"\n\n[node name=\"SearchPrev\" type=\"Button\" parent=\"Search\"]\nlayout_mode = 2\ntext = \"Prev\"\n\n[connection signal=\"pressed\" from=\"Toolbar/ShowSearch\" to=\".\" method=\"_on_ShowSearch_pressed\"]\n[connection signal=\"pressed\" from=\"Toolbar/ShowSettings\" to=\".\" method=\"_on_settings_pressed\"]\n[connection signal=\"pressed\" from=\"Toolbar/CopyButton\" to=\".\" method=\"_on_CopyButton_pressed\"]\n[connection signal=\"pressed\" from=\"Toolbar/ClearButton\" to=\".\" method=\"_on_ClearButton_pressed\"]\n[connection signal=\"pressed\" from=\"Settings/WordWrap\" to=\".\" method=\"_on_WordWrap_pressed\"]\n[connection signal=\"pressed\" from=\"Settings/UseColors\" to=\".\" method=\"_on_UseColors_pressed\"]\n[connection signal=\"focus_entered\" from=\"Search/SearchTerm\" to=\".\" method=\"_on_SearchTerm_focus_entered\"]\n[connection signal=\"gui_input\" from=\"Search/SearchTerm\" to=\".\" method=\"_on_SearchTerm_gui_input\"]\n[connection signal=\"text_changed\" from=\"Search/SearchTerm\" to=\".\" method=\"_on_SearchTerm_text_changed\"]\n[connection signal=\"text_submitted\" from=\"Search/SearchTerm\" to=\".\" method=\"_on_SearchTerm_text_entered\"]\n[connection signal=\"pressed\" from=\"Search/SearchNext\" to=\".\" method=\"_on_SearchNext_pressed\"]\n[connection signal=\"pressed\" from=\"Search/SearchPrev\" to=\".\" method=\"_on_SearchPrev_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/ResizeHandle.gd",
    "content": "@tool\nextends ColorRect\n# #############################################################################\n# Resize Handle control.  Place onto a control.  Set the orientation, then\n# set the control that this should resize.  Then you can resize the control\n# by dragging this thing around.  It's pretty neat.\n# #############################################################################\nenum ORIENTATION {\n\tLEFT,\n\tRIGHT\n}\n\n@export var orientation := ORIENTATION.RIGHT :\n\tget: return orientation\n\tset(val):\n\t\torientation = val\n\t\tqueue_redraw()\n@export var resize_control : Control = null\n@export var vertical_resize := true\n\nvar _line_width = .5\nvar _line_color = Color(.4, .4, .4)\nvar _active_line_color = Color(.3, .3, .3)\nvar _invalid_line_color = Color(1, 0, 0)\n\nvar _line_space = 3\nvar _num_lines = 8\n\nvar _mouse_down = false\n# Called when the node enters the scene tree for the first time.\n\n\nfunc _draw():\n\tvar c = _line_color\n\tif(resize_control == null):\n\t\tc = _invalid_line_color\n\telif(_mouse_down):\n\t\tc = _active_line_color\n\n\tif(orientation == ORIENTATION.LEFT):\n\t\t_draw_resize_handle_left(c)\n\telse:\n\t\t_draw_resize_handle_right(c)\n\n\nfunc _gui_input(event):\n\tif(resize_control == null):\n\t\treturn\n\n\tif(orientation == ORIENTATION.LEFT):\n\t\t_handle_left_input(event)\n\telse:\n\t\t_handle_right_input(event)\n\n\n# Draw the lines in the corner to show where you can\n# drag to resize the dialog\nfunc _draw_resize_handle_right(draw_color):\n\tvar br = size\n\n\tfor i in range(_num_lines):\n\t\tvar start = br - Vector2(i * _line_space, 0)\n\t\tvar end = br - Vector2(0, i * _line_space)\n\t\tdraw_line(start, end, draw_color, _line_width, true)\n\n\nfunc _draw_resize_handle_left(draw_color):\n\tvar bl = Vector2(0, size.y)\n\n\tfor i in range(_num_lines):\n\t\tvar start = bl + Vector2(i * _line_space, 0)\n\t\tvar end = bl -  Vector2(0, i * _line_space)\n\t\tdraw_line(start, end, draw_color, _line_width, true)\n\n\nfunc _handle_right_input(event : InputEvent):\n\tif(event is InputEventMouseMotion):\n\t\tif(_mouse_down and\n\t\t\tevent.global_position.x > 0 and\n\t\t\tevent.global_position.y < DisplayServer.window_get_size().y):\n\n\t\t\tif(vertical_resize):\n\t\t\t\tresize_control.size.y += event.relative.y\n\t\t\tresize_control.size.x += event.relative.x\n\telif(event is InputEventMouseButton):\n\t\tif(event.button_index == MOUSE_BUTTON_LEFT):\n\t\t\t_mouse_down = event.pressed\n\t\t\tqueue_redraw()\n\n\nfunc _handle_left_input(event : InputEvent):\n\tif(event is InputEventMouseMotion):\n\t\tif(_mouse_down and\n\t\t\tevent.global_position.x > 0 and\n\t\t\tevent.global_position.y < DisplayServer.window_get_size().y):\n\n\t\t\tvar start_size = resize_control.size\n\t\t\tresize_control.size.x -= event.relative.x\n\t\t\tif(resize_control.size.x != start_size.x):\n\t\t\t\tresize_control.global_position.x += event.relative.x\n\n\t\t\tif(vertical_resize):\n\t\t\t\tresize_control.size.y += event.relative.y\n\telif(event is InputEventMouseButton):\n\t\tif(event.button_index == MOUSE_BUTTON_LEFT):\n\t\t\t_mouse_down = event.pressed\n\t\t\tqueue_redraw()\n"
  },
  {
    "path": "addons/gut/gui/ResizeHandle.gd.uid",
    "content": "uid://dahg0ssw8tdx3\n"
  },
  {
    "path": "addons/gut/gui/ResizeHandle.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://bvrqqgjpyouse\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dahg0ssw8tdx3\" path=\"res://addons/gut/gui/ResizeHandle.gd\" id=\"1_oi5ed\"]\n\n[node name=\"ResizeHandle\" type=\"ColorRect\"]\ncustom_minimum_size = Vector2(20, 20)\ncolor = Color(1, 1, 1, 0)\nscript = ExtResource(\"1_oi5ed\")\n"
  },
  {
    "path": "addons/gut/gui/ResultsTree.gd",
    "content": "@tool\nextends Control\n\nvar _show_orphans = true\nvar show_orphans = true :\n\tget: return _show_orphans\n\tset(val): _show_orphans = val\n\n\nvar _hide_passing = true\nvar hide_passing = true :\n\tget: return _hide_passing\n\tset(val): _hide_passing = val\n\n\nvar _icons = {\n\tred = load('res://addons/gut/images/red.png'),\n\tgreen = load('res://addons/gut/images/green.png'),\n\tyellow = load('res://addons/gut/images/yellow.png'),\n}\nconst _col_1_bg_color = Color(0, 0, 0, .1)\nvar _max_icon_width = 10\nvar _root : TreeItem\n\n@onready var _ctrls = {\n\ttree = $Tree,\n\tlbl_overlay = $Tree/TextOverlay\n}\n\n\nsignal item_selected(script_path, inner_class, test_name, line_number)\n# -------------------\n# Private\n# -------------------\nfunc _ready():\n\t_root = _ctrls.tree.create_item()\n\t_root = _ctrls.tree.create_item()\n\t_ctrls.tree.set_hide_root(true)\n\t_ctrls.tree.columns = 2\n\t_ctrls.tree.set_column_expand(0, true)\n\t_ctrls.tree.set_column_expand(1, false)\n\t_ctrls.tree.set_column_clip_content(0, true)\n\n\t$Tree.item_selected.connect(_on_tree_item_selected)\n\n\tif(get_parent() == get_tree().root):\n\t\t_test_running_setup()\n\nfunc _test_running_setup():\n\tload_json_file('user://.gut_editor.json')\n\n\nfunc _on_tree_item_selected():\n\tvar item = _ctrls.tree.get_selected()\n\tvar item_meta = item.get_metadata(0)\n\tvar item_type = null\n\n\t# Only select the left side of the tree item, cause I like that better.\n\t# you can still click the right, but only the left gets highlighted.\n\tif(item.is_selected(1)):\n\t\titem.deselect(1)\n\t\titem.select(0)\n\n\tif(item_meta == null):\n\t\treturn\n\telse:\n\t\titem_type = item_meta.type\n\n\tvar script_path = '';\n\tvar line = -1;\n\tvar test_name = ''\n\tvar inner_class = ''\n\n\tif(item_type == 'test'):\n\t\tvar s_item = item.get_parent()\n\t\tscript_path = s_item.get_metadata(0)['path']\n\t\tinner_class = s_item.get_metadata(0)['inner_class']\n\t\tline = -1\n\t\ttest_name = item.get_text(0)\n\telif(item_type == 'assert'):\n\t\tvar s_item = item.get_parent().get_parent()\n\t\tscript_path = s_item.get_metadata(0)['path']\n\t\tinner_class = s_item.get_metadata(0)['inner_class']\n\t\tline = _get_line_number_from_assert_msg(item.get_text(0))\n\t\ttest_name = item.get_parent().get_text(0)\n\telif(item_type == 'script'):\n\t\tscript_path = item.get_metadata(0)['path']\n\t\tif(item.get_parent() != _root):\n\t\t\tinner_class = item.get_text(0)\n\t\tline = -1\n\t\ttest_name = ''\n\telse:\n\t\treturn\n\n\titem_selected.emit(script_path, inner_class, test_name, line)\n\n\nfunc _get_line_number_from_assert_msg(msg):\n\tvar line = -1\n\tif(msg.find('at line') > 0):\n\t\tline = msg.split(\"at line\")[-1].split(\" \")[-1].to_int()\n\treturn line\n\n\nfunc _get_path_and_inner_class_name_from_test_path(path):\n\tvar to_return = {\n\t\tpath = '',\n\t\tinner_class = ''\n\t}\n\n\tto_return.path = path\n\tif !path.ends_with('.gd'):\n\t\tvar loc = path.find('.gd')\n\t\tto_return.inner_class = path.split('.')[-1]\n\t\tto_return.path = path.substr(0, loc + 3)\n\treturn to_return\n\n\nfunc _find_script_item_with_path(path):\n\tvar items = _root.get_children()\n\tvar to_return = null\n\n\tvar idx = 0\n\twhile(idx < items.size() and to_return == null):\n\t\tvar item = items[idx]\n\t\tif(item.get_metadata(0).path == path):\n\t\t\tto_return = item\n\t\telse:\n\t\t\tidx += 1\n\n\treturn to_return\n\n\nfunc _add_script_tree_item(script_path, script_json):\n\tvar path_info = _get_path_and_inner_class_name_from_test_path(script_path)\n\tvar item_text = script_path\n\tvar parent = _root\n\n\tif(path_info.inner_class != ''):\n\t\tparent = _find_script_item_with_path(path_info.path)\n\t\titem_text = path_info.inner_class\n\t\tif(parent == null):\n\t\t\tparent = _add_script_tree_item(path_info.path, {})\n\n\t\tparent.get_metadata(0).inner_tests += script_json['props']['tests']\n\t\tparent.get_metadata(0).inner_passing += script_json['props']['tests']\n\t\tparent.get_metadata(0).inner_passing -= script_json['props']['failures']\n\t\tparent.get_metadata(0).inner_passing -= script_json['props']['pending']\n\n\t\tvar total_text = str(\"All \", parent.get_metadata(0).inner_tests, \" passed\")\n\t\tif(parent.get_metadata(0).inner_passing != parent.get_metadata(0).inner_tests):\n\t\t\ttotal_text = str(parent.get_metadata(0).inner_passing, '/', parent.get_metadata(0).inner_tests, ' passed.')\n\t\tparent.set_text(1, total_text)\n\n\tvar item = _ctrls.tree.create_item(parent)\n\titem.set_text(0, item_text)\n\tvar meta = {\n\t\t\"type\":\"script\",\n\t\t\"path\":path_info.path,\n\t\t\"inner_class\":path_info.inner_class,\n\t\t\"json\":script_json,\n\t\t\"inner_passing\":0,\n\t\t\"inner_tests\":0\n\t}\n\titem.set_metadata(0, meta)\n\titem.set_custom_bg_color(1, _col_1_bg_color)\n\n\treturn item\n\n\nfunc _add_assert_item(text, icon, parent_item):\n\t# print('        * adding assert')\n\tvar assert_item = _ctrls.tree.create_item(parent_item)\n\tassert_item.set_icon_max_width(0, _max_icon_width)\n\tassert_item.set_text(0, text)\n\tassert_item.set_metadata(0, {\"type\":\"assert\"})\n\tassert_item.set_icon(0, icon)\n\tassert_item.set_custom_bg_color(1, _col_1_bg_color)\n\n\treturn assert_item\n\n\nfunc _add_test_tree_item(test_name, test_json, script_item):\n\t# print('    * adding test ', test_name)\n\tvar no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphans == 0)\n\tif(_hide_passing and test_json['status'] == 'pass' and no_orphans_to_show):\n\t\treturn\n\n\tvar item = _ctrls.tree.create_item(script_item)\n\tvar status = test_json['status']\n\tvar meta = {\"type\":\"test\", \"json\":test_json}\n\n\titem.set_text(0, test_name)\n\titem.set_text(1, status)\n\titem.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)\n\titem.set_custom_bg_color(1, _col_1_bg_color)\n\n\titem.set_metadata(0, meta)\n\titem.set_icon_max_width(0, _max_icon_width)\n\n\tvar orphan_text = 'orphans'\n\tif(test_json.orphans == 1):\n\t\torphan_text = 'orphan'\n\torphan_text = str(test_json.orphans, ' ', orphan_text)\n\n\tif(status == 'pass' and no_orphans_to_show):\n\t\titem.set_icon(0, _icons.green)\n\telif(status == 'pass' and !no_orphans_to_show):\n\t\titem.set_icon(0, _icons.yellow)\n\t\titem.set_text(1, orphan_text)\n\telif(status == 'fail'):\n\t\titem.set_icon(0, _icons.red)\n\telse:\n\t\titem.set_icon(0, _icons.yellow)\n\n\tif(!_hide_passing):\n\t\tfor passing in test_json.passing:\n\t\t\t_add_assert_item('pass: ' + passing, _icons.green, item)\n\n\tfor failure in test_json.failing:\n\t\t_add_assert_item(\"fail:  \" + failure.replace(\"\\n\", ''), _icons.red, item)\n\n\tfor pending in test_json.pending:\n\t\t_add_assert_item(\"pending:  \" + pending.replace(\"\\n\", ''), _icons.yellow, item)\n\n\tif(status != 'pass' and !no_orphans_to_show):\n\t\t_add_assert_item(orphan_text, _icons.yellow, item)\n\n\treturn item\n\n\nfunc _add_script_to_tree(key, script_json):\n\tvar tests = script_json['tests']\n\tvar test_keys = tests.keys()\n\tvar s_item = _add_script_tree_item(key, script_json)\n\tvar bad_count = 0\n\n\tfor test_key in test_keys:\n\t\tvar t_item = _add_test_tree_item(test_key, tests[test_key], s_item)\n\t\tif(tests[test_key].status != 'pass'):\n\t\t\tbad_count += 1\n\t\telif(t_item != null):\n\t\t\tt_item.collapsed = true\n\n\tif(s_item.get_children().size() == 0):\n\t\ts_item.free()\n\telse:\n\t\tvar total_text = str('All ', test_keys.size(), ' passed')\n\t\tif(bad_count == 0):\n\t\t\ts_item.collapsed = true\n\t\telse:\n\t\t\ttotal_text = str(test_keys.size() - bad_count, '/', test_keys.size(), ' passed')\n\t\ts_item.set_text(1, total_text)\n\n\nfunc _free_childless_scripts():\n\tvar items = _root.get_children()\n\tfor item in items:\n\t\tvar next_item = item.get_next()\n\t\tif(item.get_children().size() == 0):\n\t\t\titem.free()\n\t\titem = next_item\n\n\nfunc _show_all_passed():\n\tif(_root.get_children().size() == 0):\n\t\tadd_centered_text('Everything passed!')\n\n\nfunc _load_result_tree(j):\n\tvar scripts = j['test_scripts']['scripts']\n\tvar script_keys = scripts.keys()\n\t# if we made it here, the json is valid and we did something, otherwise the\n\t# 'nothing to see here' should be visible.\n\tclear_centered_text()\n\n\tvar add_count = 0\n\tfor key in script_keys:\n\t\tif(scripts[key]['props']['tests'] > 0):\n\t\t\tadd_count += 1\n\t\t\t_add_script_to_tree(key, scripts[key])\n\n\t_free_childless_scripts()\n\tif(add_count == 0):\n\t\tadd_centered_text('Nothing was run')\n\telse:\n\t\t_show_all_passed()\n\n\n# -------------------\n# Public\n# -------------------\nfunc load_json_file(path):\n\tvar file = FileAccess.open(path, FileAccess.READ)\n\tvar text = ''\n\tif(file != null):\n\t\ttext = file.get_as_text()\n\n\tif(text != ''):\n\t\tvar test_json_conv = JSON.new()\n\t\tvar result = test_json_conv.parse(text)\n\t\tif(result != OK):\n\t\t\tadd_centered_text(str(path, \" has invalid json in it \\n\",\n\t\t\t\t'Error ', result, \"@\", test_json_conv.get_error_line(), \"\\n\",\n\t\t\t\ttest_json_conv.get_error_message()))\n\t\t\treturn\n\n\t\tvar data = test_json_conv.get_data()\n\t\tload_json_results(data)\n\telse:\n\t\tadd_centered_text(str(path, ' was empty or does not exist.'))\n\n\nfunc load_json_results(j):\n\tclear()\n\t_load_result_tree(j)\n\n\nfunc clear():\n\t_ctrls.tree.clear()\n\t_root = _ctrls.tree.create_item()\n\n\nfunc set_summary_min_width(width):\n\t_ctrls.tree.set_column_custom_minimum_width(1, width)\n\n\nfunc add_centered_text(t):\n\t_ctrls.lbl_overlay.visible = true\n\t_ctrls.lbl_overlay.text = t\n\n\nfunc clear_centered_text():\n\t_ctrls.lbl_overlay.visible = false\n\t_ctrls.lbl_overlay.text = ''\n\n\nfunc collapse_all():\n\tset_collapsed_on_all(_root, true)\n\n\nfunc expand_all():\n\tset_collapsed_on_all(_root, false)\n\n\nfunc set_collapsed_on_all(item, value):\n\titem.set_collapsed_recursive(value)\n\tif(item == _root and value):\n\t\titem.set_collapsed(false)\n\n\nfunc get_selected():\n\treturn _ctrls.tree.get_selected()\n"
  },
  {
    "path": "addons/gut/gui/ResultsTree.gd.uid",
    "content": "uid://b6e871qdu7x1w\n"
  },
  {
    "path": "addons/gut/gui/ResultsTree.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://dls5r5f6157nq\"]\n\n[ext_resource type=\"Script\" uid=\"uid://b6e871qdu7x1w\" path=\"res://addons/gut/gui/ResultsTree.gd\" id=\"1_b4uub\"]\n\n[node name=\"ResultsTree\" type=\"VBoxContainer\"]\ncustom_minimum_size = Vector2(10, 10)\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_right = -70.0\noffset_bottom = -104.0\ngrow_horizontal = 2\ngrow_vertical = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nscript = ExtResource(\"1_b4uub\")\n\n[node name=\"Tree\" type=\"Tree\" parent=\".\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\ncolumns = 2\nhide_root = true\n\n[node name=\"TextOverlay\" type=\"Label\" parent=\"Tree\"]\nvisible = false\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\n"
  },
  {
    "path": "addons/gut/gui/RunAtCursor.gd",
    "content": "@tool\nextends Control\n\n\nvar ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')\n\n@onready var _ctrls = {\n\tbtn_script = $HBox/BtnRunScript,\n\tbtn_inner = $HBox/BtnRunInnerClass,\n\tbtn_method = $HBox/BtnRunMethod,\n\tlbl_none = $HBox/LblNoneSelected,\n\tarrow_1 = $HBox/Arrow1,\n\tarrow_2 = $HBox/Arrow2\n}\n\nvar _editors = null\nvar _cur_editor = null\nvar _last_line = -1\nvar _cur_script_path = null\nvar _last_info = {\n\tscript = null,\n\tinner_class = null,\n\ttest_method = null\n}\n\n\nsignal run_tests(what)\n\n\nfunc _ready():\n\t_ctrls.lbl_none.visible = true\n\t_ctrls.btn_script.visible = false\n\t_ctrls.btn_inner.visible = false\n\t_ctrls.btn_method.visible = false\n\t_ctrls.arrow_1.visible = false\n\t_ctrls.arrow_2.visible = false\n\n# ----------------\n# Private\n# ----------------\nfunc _set_editor(which):\n\t_last_line = -1\n\tif(_cur_editor != null and _cur_editor.get_ref()):\n\t\t# _cur_editor.get_ref().disconnect('cursor_changed',Callable(self,'_on_cursor_changed'))\n\t\t_cur_editor.get_ref().caret_changed.disconnect(_on_cursor_changed)\n\n\tif(which != null):\n\t\t_cur_editor = weakref(which)\n\t\twhich.caret_changed.connect(_on_cursor_changed.bind(which))\n\t\t# which.connect('cursor_changed',Callable(self,'_on_cursor_changed'),[which])\n\n\t\t_last_line = which.get_caret_line()\n\t\t_last_info = _editors.get_line_info()\n\t\t_update_buttons(_last_info)\n\n\nfunc _update_buttons(info):\n\t_ctrls.lbl_none.visible = _cur_script_path == null\n\t_ctrls.btn_script.visible = _cur_script_path != null\n\n\t_ctrls.btn_inner.visible = info.inner_class != null\n\t_ctrls.arrow_1.visible = info.inner_class != null\n\t_ctrls.btn_inner.text = str(info.inner_class)\n\t_ctrls.btn_inner.tooltip_text = str(\"Run all tests in Inner-Test-Class \", info.inner_class)\n\n\t_ctrls.btn_method.visible = info.test_method != null\n\t_ctrls.arrow_2.visible = info.test_method != null\n\t_ctrls.btn_method.text = str(info.test_method)\n\t_ctrls.btn_method.tooltip_text = str(\"Run test \", info.test_method)\n\n\t# The button's new size won't take effect until the next frame.\n\t# This appears to be what was causing the button to not be clickable the\n\t# first time.\n\tcall_deferred(\"_update_size\")\n\nfunc _update_size():\n\tcustom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x\n\n# ----------------\n# Events\n# ----------------\nfunc _on_cursor_changed(which):\n\tif(which.get_caret_line() != _last_line):\n\t\t_last_line = which.get_caret_line()\n\t\t_last_info = _editors.get_line_info()\n\t\t_update_buttons(_last_info)\n\n\nfunc _on_BtnRunScript_pressed():\n\tvar info = _last_info.duplicate()\n\tinfo.script = _cur_script_path.get_file()\n\tinfo.inner_class = null\n\tinfo.test_method = null\n\temit_signal(\"run_tests\", info)\n\n\nfunc _on_BtnRunInnerClass_pressed():\n\tvar info = _last_info.duplicate()\n\tinfo.script = _cur_script_path.get_file()\n\tinfo.test_method = null\n\temit_signal(\"run_tests\", info)\n\n\nfunc _on_BtnRunMethod_pressed():\n\tvar info = _last_info.duplicate()\n\tinfo.script = _cur_script_path.get_file()\n\temit_signal(\"run_tests\", info)\n\n\n# ----------------\n# Public\n# ----------------\nfunc set_script_text_editors(value):\n\t_editors = value\n\n\nfunc activate_for_script(path):\n\t_ctrls.btn_script.visible = true\n\t_ctrls.btn_script.text = path.get_file()\n\t_ctrls.btn_script.tooltip_text = str(\"Run all tests in script \", path)\n\t_cur_script_path = path\n\t_editors.refresh()\n\t# We have to wait a beat for the visibility to change on\n\t# the editors, otherwise we always get the first one.\n\tawait get_tree().process_frame\n\t_set_editor(_editors.get_current_text_edit())\n\n\nfunc get_script_button():\n\treturn _ctrls.btn_script\n\n\nfunc get_inner_button():\n\treturn _ctrls.btn_inner\n\n\nfunc get_test_button():\n\treturn _ctrls.btn_method\n\n\n# not used, thought was configurable but it's just the script prefix\nfunc set_method_prefix(value):\n\t_editors.set_method_prefix(value)\n\n\n# not used, thought was configurable but it's just the script prefix\nfunc set_inner_class_prefix(value):\n\t_editors.set_inner_class_prefix(value)\n\n\n# Mashed this function in here b/c it has _editors.  Probably should be\n# somewhere else (possibly in script_text_editor_controls).\nfunc search_current_editor_for_text(txt):\n\tvar te = _editors.get_current_text_edit()\n\tvar result = te.search(txt, 0, 0, 0)\n\tvar to_return = -1\n\n\treturn to_return\n"
  },
  {
    "path": "addons/gut/gui/RunAtCursor.gd.uid",
    "content": "uid://djglbxlh2shog\n"
  },
  {
    "path": "addons/gut/gui/RunAtCursor.tscn",
    "content": "[gd_scene load_steps=4 format=3 uid=\"uid://0yunjxtaa8iw\"]\n\n[ext_resource type=\"Script\" uid=\"uid://djglbxlh2shog\" path=\"res://addons/gut/gui/RunAtCursor.gd\" id=\"1\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cr6tvdv0ve6cv\" path=\"res://addons/gut/gui/play.png\" id=\"2\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://6wra5rxmfsrl\" path=\"res://addons/gut/gui/arrow.png\" id=\"3\"]\n\n[node name=\"RunAtCursor\" type=\"Control\"]\nlayout_mode = 3\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\noffset_right = 1.0\noffset_bottom = -527.0\ngrow_horizontal = 2\ngrow_vertical = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\nscript = ExtResource(\"1\")\n\n[node name=\"HBox\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"LblNoneSelected\" type=\"Label\" parent=\"HBox\"]\nlayout_mode = 2\ntext = \"<None>\"\n\n[node name=\"BtnRunScript\" type=\"Button\" parent=\"HBox\"]\nvisible = false\nlayout_mode = 2\ntext = \"<script>\"\nicon = ExtResource(\"2\")\n\n[node name=\"Arrow1\" type=\"TextureButton\" parent=\"HBox\"]\nvisible = false\ncustom_minimum_size = Vector2(24, 0)\nlayout_mode = 2\ntexture_normal = ExtResource(\"3\")\nstretch_mode = 3\n\n[node name=\"BtnRunInnerClass\" type=\"Button\" parent=\"HBox\"]\nvisible = false\nlayout_mode = 2\ntext = \"<inner class>\"\nicon = ExtResource(\"2\")\n\n[node name=\"Arrow2\" type=\"TextureButton\" parent=\"HBox\"]\nvisible = false\ncustom_minimum_size = Vector2(24, 0)\nlayout_mode = 2\ntexture_normal = ExtResource(\"3\")\nstretch_mode = 3\n\n[node name=\"BtnRunMethod\" type=\"Button\" parent=\"HBox\"]\nvisible = false\nlayout_mode = 2\ntext = \"<method>\"\nicon = ExtResource(\"2\")\n\n[connection signal=\"pressed\" from=\"HBox/BtnRunScript\" to=\".\" method=\"_on_BtnRunScript_pressed\"]\n[connection signal=\"pressed\" from=\"HBox/BtnRunInnerClass\" to=\".\" method=\"_on_BtnRunInnerClass_pressed\"]\n[connection signal=\"pressed\" from=\"HBox/BtnRunMethod\" to=\".\" method=\"_on_BtnRunMethod_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/RunResults.gd",
    "content": "@tool\nextends Control\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\n\nvar _interface = null\nvar _font = null\nvar _font_size = null\nvar _editors = null # script_text_editor_controls.gd\nvar _output_control = null\n\n@onready var _ctrls = {\n\ttree = $VBox/Output/Scroll/Tree,\n\ttoolbar = {\n\t\ttoolbar = $VBox/Toolbar,\n\t\tcollapse = $VBox/Toolbar/Collapse,\n\t\tcollapse_all = $VBox/Toolbar/CollapseAll,\n\t\texpand = $VBox/Toolbar/Expand,\n\t\texpand_all = $VBox/Toolbar/ExpandAll,\n\t\thide_passing = $VBox/Toolbar/HidePassing,\n\t\tshow_script = $VBox/Toolbar/ShowScript,\n\t\tscroll_output = $VBox/Toolbar/ScrollOutput\n\t}\n}\n\nfunc _ready():\n\tvar f = null\n\tif ($FontSampler.get_label_settings() == null) :\n\t\tf = get_theme_default_font()\n\telse :\n\t\tf = $FontSampler.get_label_settings().font\n\tvar s_size = f.get_string_size(\"000 of 000 passed\")\n\t_ctrls.tree.set_summary_min_width(s_size.x)\n\n\t_set_toolbutton_icon(_ctrls.toolbar.collapse, 'CollapseTree', 'c')\n\t_set_toolbutton_icon(_ctrls.toolbar.collapse_all, 'CollapseTree', 'c')\n\t_set_toolbutton_icon(_ctrls.toolbar.expand, 'ExpandTree', 'e')\n\t_set_toolbutton_icon(_ctrls.toolbar.expand_all, 'ExpandTree', 'e')\n\t_set_toolbutton_icon(_ctrls.toolbar.show_script, 'Script', 'ss')\n\t_set_toolbutton_icon(_ctrls.toolbar.scroll_output, 'Font', 'so')\n\n\t_ctrls.tree.hide_passing = true\n\t_ctrls.toolbar.hide_passing.button_pressed = false\n\t_ctrls.tree.show_orphans = true\n\t_ctrls.tree.item_selected.connect(_on_item_selected)\n\n\tif(get_parent() == get_tree().root):\n\t\t_test_running_setup()\n\n\tcall_deferred('_update_min_width')\n\n\nfunc _test_running_setup():\n\t_ctrls.tree.hide_passing = true\n\t_ctrls.tree.show_orphans = true\n\n\t_ctrls.toolbar.hide_passing.text = '[hp]'\n\t_ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path)\n\n\nfunc _set_toolbutton_icon(btn, icon_name, text):\n\tif(Engine.is_editor_hint()):\n\t\tbtn.icon = get_theme_icon(icon_name, 'EditorIcons')\n\telse:\n\t\tbtn.text = str('[', text, ']')\n\n\nfunc _update_min_width():\n\tcustom_minimum_size.x = _ctrls.toolbar.toolbar.size.x\n\n\nfunc _open_script_in_editor(path, line_number):\n\tif(_interface == null):\n\t\tprint('Too soon, wait a bit and try again.')\n\t\treturn\n\n\tvar r = load(path)\n\tif(line_number != null and line_number != -1):\n\t\t_interface.edit_script(r, line_number)\n\telse:\n\t\t_interface.edit_script(r)\n\n\tif(_ctrls.toolbar.show_script.pressed):\n\t\t_interface.set_main_screen_editor('Script')\n\n\n# starts at beginning of text edit and searches for each search term, moving\n# through the text as it goes; ensuring that, when done, it found the first\n# occurance of the last srting that happend after the first occurance of\n# each string before it.  (Generic way of searching for a method name in an\n# inner class that may have be a duplicate of a method name in a different\n# inner class)\nfunc _get_line_number_for_seq_search(search_strings, te):\n\tif(te == null):\n\t\tprint(\"No Text editor to get line number for\")\n\t\treturn 0;\n\n\tvar result = null\n\tvar line = Vector2i(0, 0)\n\tvar s_flags = 0\n\n\tvar i = 0\n\tvar string_found = true\n\twhile(i < search_strings.size() and string_found):\n\t\tresult = te.search(search_strings[i], s_flags, line.y, line.x)\n\t\tif(result.x != -1):\n\t\t\tline = result\n\t\telse:\n\t\t\tstring_found = false\n\t\ti += 1\n\n\treturn line.y\n\n\nfunc _goto_code(path, line, method_name='', inner_class =''):\n\tif(_interface == null):\n\t\tprint('going to ', [path, line, method_name, inner_class])\n\t\treturn\n\n\t_open_script_in_editor(path, line)\n\tif(line == -1):\n\t\tvar search_strings = []\n\t\tif(inner_class != ''):\n\t\t\tsearch_strings.append(inner_class)\n\n\t\tif(method_name != ''):\n\t\t\tsearch_strings.append(method_name)\n\n\t\tawait get_tree().process_frame\n\t\tline = _get_line_number_for_seq_search(search_strings, _editors.get_current_text_edit())\n\t\tif(line != null and line != -1):\n\t\t\t_interface.get_script_editor().goto_line(line)\n\n\nfunc _goto_output(path, method_name, inner_class):\n\tif(_output_control == null):\n\t\treturn\n\n\tvar search_strings = [path]\n\n\tif(inner_class != ''):\n\t\tsearch_strings.append(inner_class)\n\n\tif(method_name != ''):\n\t\tsearch_strings.append(method_name)\n\n\tvar line = _get_line_number_for_seq_search(search_strings, _output_control.get_rich_text_edit())\n\tif(line != null and line != -1):\n\t\t_output_control.scroll_to_line(line)\n\n\n\n\n# --------------\n# Events\n# --------------\nfunc _on_Collapse_pressed():\n\tcollapse_selected()\n\n\nfunc _on_Expand_pressed():\n\texpand_selected()\n\n\nfunc _on_CollapseAll_pressed():\n\tcollapse_all()\n\n\nfunc _on_ExpandAll_pressed():\n\texpand_all()\n\n\nfunc _on_Hide_Passing_pressed():\n\t_ctrls.tree.hide_passing = !_ctrls.toolbar.hide_passing.button_pressed\n\t_ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path)\n\n\nfunc _on_item_selected(script_path, inner_class, test_name, line):\n\tif(_ctrls.toolbar.show_script.button_pressed):\n\t\t_goto_code(script_path, line, test_name, inner_class)\n\tif(_ctrls.toolbar.scroll_output.button_pressed):\n\t\t_goto_output(script_path, test_name, inner_class)\n\n\n\n\n# --------------\n# Public\n# --------------\nfunc add_centered_text(t):\n\t_ctrls.tree.add_centered_text(t)\n\n\nfunc clear_centered_text():\n\t_ctrls.tree.clear_centered_text()\n\n\nfunc clear():\n\t_ctrls.tree.clear()\n\tclear_centered_text()\n\n\nfunc set_interface(which):\n\t_interface = which\n\n\nfunc set_script_text_editors(value):\n\t_editors = value\n\n\nfunc collapse_all():\n\t_ctrls.tree.collapse_all()\n\n\nfunc expand_all():\n\t_ctrls.tree.expand_all()\n\n\nfunc collapse_selected():\n\tvar item = _ctrls.tree.get_selected()\n\tif(item != null):\n\t\t_ctrls.tree.set_collapsed_on_all(item, true)\n\n\nfunc expand_selected():\n\tvar item = _ctrls.tree.get_selected()\n\tif(item != null):\n\t\t_ctrls.tree.set_collapsed_on_all(item, false)\n\n\nfunc set_show_orphans(should):\n\t_ctrls.tree.show_orphans = should\n\n\nfunc set_font(font_name, size):\n\tpass\n#\tvar dyn_font = FontFile.new()\n#\tvar font_data = FontFile.new()\n#\tfont_data.font_path = 'res://addons/gut/fonts/' + font_name + '-Regular.ttf'\n#\tfont_data.antialiased = true\n#\tdyn_font.font_data = font_data\n#\n#\t_font = dyn_font\n#\t_font.size = size\n#\t_font_size = size\n\n\nfunc set_output_control(value):\n\t_output_control = value\n\n\nfunc load_json_results(j):\n\t_ctrls.tree.load_json_results(j)\n"
  },
  {
    "path": "addons/gut/gui/RunResults.gd.uid",
    "content": "uid://dq5y5rt75836n\n"
  },
  {
    "path": "addons/gut/gui/RunResults.tscn",
    "content": "[gd_scene load_steps=5 format=3 uid=\"uid://4gyyn12um08h\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dq5y5rt75836n\" path=\"res://addons/gut/gui/RunResults.gd\" id=\"1\"]\n[ext_resource type=\"PackedScene\" uid=\"uid://dls5r5f6157nq\" path=\"res://addons/gut/gui/ResultsTree.tscn\" id=\"2_o808v\"]\n\n[sub_resource type=\"Image\" id=\"Image_p7oqn\"]\ndata = {\n\"data\": PackedByteArray(255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 42, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 93, 93, 41, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 92, 92, 0, 255, 92, 92, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 93, 93, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0),\n\"format\": \"RGBA8\",\n\"height\": 16,\n\"mipmaps\": false,\n\"width\": 16\n}\n\n[sub_resource type=\"ImageTexture\" id=\"ImageTexture_srqj5\"]\nimage = SubResource(\"Image_p7oqn\")\n\n[node name=\"RunResults\" type=\"Control\"]\ncustom_minimum_size = Vector2(345, 0)\nlayout_mode = 3\nanchors_preset = 0\noffset_right = 709.0\noffset_bottom = 321.0\nscript = ExtResource(\"1\")\n\n[node name=\"VBox\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\n\n[node name=\"Toolbar\" type=\"HBoxContainer\" parent=\"VBox\"]\nlayout_mode = 2\nsize_flags_horizontal = 0\n\n[node name=\"Expand\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Collapse\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Sep\" type=\"ColorRect\" parent=\"VBox/Toolbar\"]\ncustom_minimum_size = Vector2(2, 0)\nlayout_mode = 2\n\n[node name=\"LblAll\" type=\"Label\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\ntext = \"All:\"\n\n[node name=\"ExpandAll\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"CollapseAll\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Sep2\" type=\"ColorRect\" parent=\"VBox/Toolbar\"]\ncustom_minimum_size = Vector2(2, 0)\nlayout_mode = 2\n\n[node name=\"HidePassing\" type=\"CheckBox\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\nsize_flags_horizontal = 4\ntext = \"Passing\"\n\n[node name=\"Sep3\" type=\"ColorRect\" parent=\"VBox/Toolbar\"]\ncustom_minimum_size = Vector2(2, 0)\nlayout_mode = 2\n\n[node name=\"LblSync\" type=\"Label\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\ntext = \"Sync:\"\n\n[node name=\"ShowScript\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"ScrollOutput\" type=\"Button\" parent=\"VBox/Toolbar\"]\nlayout_mode = 2\ntoggle_mode = true\nbutton_pressed = true\nicon = SubResource(\"ImageTexture_srqj5\")\n\n[node name=\"Output\" type=\"Panel\" parent=\"VBox\"]\nself_modulate = Color(1, 1, 1, 0.541176)\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[node name=\"Scroll\" type=\"ScrollContainer\" parent=\"VBox/Output\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\n\n[node name=\"Tree\" parent=\"VBox/Output/Scroll\" instance=ExtResource(\"2_o808v\")]\nlayout_mode = 2\n\n[node name=\"FontSampler\" type=\"Label\" parent=\".\"]\nvisible = false\nlayout_mode = 0\noffset_right = 40.0\noffset_bottom = 14.0\ntext = \"000 of 000 passed\"\n\n[connection signal=\"pressed\" from=\"VBox/Toolbar/Expand\" to=\".\" method=\"_on_Expand_pressed\"]\n[connection signal=\"pressed\" from=\"VBox/Toolbar/Collapse\" to=\".\" method=\"_on_Collapse_pressed\"]\n[connection signal=\"pressed\" from=\"VBox/Toolbar/ExpandAll\" to=\".\" method=\"_on_ExpandAll_pressed\"]\n[connection signal=\"pressed\" from=\"VBox/Toolbar/CollapseAll\" to=\".\" method=\"_on_CollapseAll_pressed\"]\n[connection signal=\"pressed\" from=\"VBox/Toolbar/HidePassing\" to=\".\" method=\"_on_Hide_Passing_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/Settings.tscn",
    "content": "[gd_scene format=3 uid=\"uid://cvvvtsah38l0e\"]\n\n[node name=\"Settings\" type=\"VBoxContainer\"]\noffset_right = 388.0\noffset_bottom = 586.0\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n"
  },
  {
    "path": "addons/gut/gui/ShortcutButton.gd",
    "content": "@tool\nextends Control\n\n\n@onready var _ctrls = {\n\tshortcut_label = $Layout/lblShortcut,\n\tset_button = $Layout/SetButton,\n\tsave_button = $Layout/SaveButton,\n\tcancel_button = $Layout/CancelButton,\n\tclear_button = $Layout/ClearButton\n}\n\nsignal changed\nsignal start_edit\nsignal end_edit\n\nconst NO_SHORTCUT = '<None>'\n\nvar _source_event = InputEventKey.new()\nvar _pre_edit_event = null\nvar _key_disp = NO_SHORTCUT\nvar _editing = false\n\nvar _modifier_keys = [KEY_ALT, KEY_CTRL, KEY_META, KEY_SHIFT]\n\n# Called when the node enters the scene tree for the first time.\nfunc _ready():\n\tset_process_unhandled_key_input(false)\n\n\nfunc _display_shortcut():\n\tif(_key_disp == ''):\n\t\t_key_disp = NO_SHORTCUT\n\t_ctrls.shortcut_label.text = _key_disp\n\n\nfunc _is_shift_only_modifier():\n\treturn _source_event.shift_pressed and \\\n\t\t!(_source_event.alt_pressed or \\\n\t\t\t_source_event.ctrl_pressed or \\\n\t\t\t_source_event.meta_pressed) \\\n\t\tand !_is_modifier(_source_event.keycode)\n\n\nfunc _has_modifier(event):\n\treturn event.alt_pressed or event.ctrl_pressed or \\\n\t\tevent.meta_pressed or event.shift_pressed\n\n\nfunc _is_modifier(keycode):\n\treturn _modifier_keys.has(keycode)\n\n\nfunc _edit_mode(should):\n\t_editing = should\n\tset_process_unhandled_key_input(should)\n\t_ctrls.set_button.visible = !should\n\t_ctrls.save_button.visible = should\n\t_ctrls.save_button.disabled = should\n\t_ctrls.cancel_button.visible = should\n\t_ctrls.clear_button.visible = !should\n\n\tif(should and to_s() == ''):\n\t\t_ctrls.shortcut_label.text = 'press buttons'\n\telse:\n\t\t_ctrls.shortcut_label.text = to_s()\n\n\tif(should):\n\t\temit_signal(\"start_edit\")\n\telse:\n\t\temit_signal(\"end_edit\")\n\n# ---------------\n# Events\n# ---------------\nfunc _unhandled_key_input(event):\n\tif(event is InputEventKey):\n\t\tif(event.pressed):\n\t\t\tif(_has_modifier(event) and !_is_modifier(event.get_keycode_with_modifiers())):\n\t\t\t\t_source_event = event\n\t\t\t\t_key_disp = OS.get_keycode_string(event.get_keycode_with_modifiers())\n\t\t\telse:\n\t\t\t\t_source_event = InputEventKey.new()\n\t\t\t\t_key_disp = NO_SHORTCUT\n\t\t\t_display_shortcut()\n\t\t\t_ctrls.save_button.disabled = !is_valid()\n\n\nfunc _on_SetButton_pressed():\n\t_pre_edit_event = _source_event.duplicate(true)\n\t_edit_mode(true)\n\n\nfunc _on_SaveButton_pressed():\n\t_edit_mode(false)\n\t_pre_edit_event = null\n\temit_signal('changed')\n\n\nfunc _on_CancelButton_pressed():\n\tcancel()\n\n\nfunc _on_ClearButton_pressed():\n\tclear_shortcut()\n\n# ---------------\n# Public\n# ---------------\nfunc to_s():\n\treturn OS.get_keycode_string(_source_event.get_keycode_with_modifiers())\n\n\nfunc is_valid():\n\treturn _has_modifier(_source_event) and !_is_shift_only_modifier()\n\n\nfunc get_shortcut():\n\tvar to_return = Shortcut.new()\n\tto_return.events.append(_source_event)\n\treturn to_return\n\n\nfunc set_shortcut(sc):\n\tif(sc == null or sc.events == null || sc.events.size() <= 0):\n\t\tclear_shortcut()\n\telse:\n\t\t_source_event = sc.events[0]\n\t\t_key_disp = to_s()\n\t\t_display_shortcut()\n\n\nfunc clear_shortcut():\n\t_source_event = InputEventKey.new()\n\t_key_disp = NO_SHORTCUT\n\t_display_shortcut()\n\n\nfunc disable_set(should):\n\t_ctrls.set_button.disabled = should\n\n\nfunc disable_clear(should):\n\t_ctrls.clear_button.disabled = should\n\t\n\t\nfunc cancel():\n\tif(_editing):\n\t\t_edit_mode(false)\n\t\t_source_event = _pre_edit_event\n\t\t_key_disp = to_s()\n\t\t_display_shortcut()\n"
  },
  {
    "path": "addons/gut/gui/ShortcutButton.gd.uid",
    "content": "uid://slt6m4nclynh\n"
  },
  {
    "path": "addons/gut/gui/ShortcutButton.tscn",
    "content": "[gd_scene load_steps=2 format=3 uid=\"uid://sfb1fw8j6ufu\"]\n\n[ext_resource type=\"Script\" uid=\"uid://slt6m4nclynh\" path=\"res://addons/gut/gui/ShortcutButton.gd\" id=\"1\"]\n\n[node name=\"ShortcutButton\" type=\"Control\"]\ncustom_minimum_size = Vector2(210, 30)\nlayout_mode = 3\nanchor_right = 0.123\nanchor_bottom = 0.04\noffset_right = 68.304\noffset_bottom = 6.08\nscript = ExtResource(\"1\")\n\n[node name=\"Layout\" type=\"HBoxContainer\" parent=\".\"]\nlayout_mode = 0\nanchor_right = 1.0\nanchor_bottom = 1.0\n\n[node name=\"lblShortcut\" type=\"Label\" parent=\"Layout\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 7\ntext = \"<None>\"\nhorizontal_alignment = 2\n\n[node name=\"CenterContainer\" type=\"CenterContainer\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(10, 0)\nlayout_mode = 2\n\n[node name=\"SetButton\" type=\"Button\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\ntext = \"Set\"\n\n[node name=\"SaveButton\" type=\"Button\" parent=\"Layout\"]\nvisible = false\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\ntext = \"Save\"\n\n[node name=\"CancelButton\" type=\"Button\" parent=\"Layout\"]\nvisible = false\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\ntext = \"Cancel\"\n\n[node name=\"ClearButton\" type=\"Button\" parent=\"Layout\"]\ncustom_minimum_size = Vector2(60, 0)\nlayout_mode = 2\ntext = \"Clear\"\n\n[connection signal=\"pressed\" from=\"Layout/SetButton\" to=\".\" method=\"_on_SetButton_pressed\"]\n[connection signal=\"pressed\" from=\"Layout/SaveButton\" to=\".\" method=\"_on_SaveButton_pressed\"]\n[connection signal=\"pressed\" from=\"Layout/CancelButton\" to=\".\" method=\"_on_CancelButton_pressed\"]\n[connection signal=\"pressed\" from=\"Layout/ClearButton\" to=\".\" method=\"_on_ClearButton_pressed\"]\n"
  },
  {
    "path": "addons/gut/gui/arrow.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://6wra5rxmfsrl\"\npath=\"res://.godot/imported/arrow.png-2b5b2d838b5b3467cf300ac2da1630d9.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/gui/arrow.png\"\ndest_files=[\"res://.godot/imported/arrow.png-2b5b2d838b5b3467cf300ac2da1630d9.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/gui/editor_globals.gd",
    "content": "@tool\n\nstatic var GutUserPreferences = load(\"res://addons/gut/gui/gut_user_preferences.gd\")\nstatic var temp_directory = 'user://gut_temp_directory'\n\nstatic var editor_run_gut_config_path = 'gut_editor_config.json':\n\t# This avoids having to use path_join wherever we want to reference this\n\t# path.  The value is not supposed to change.  Could it be a constant\n\t# instead?  Probably, but I didn't like repeating the directory part.\n\t# Do I like that this is a bit witty.  Absolutely.\n\tget: return temp_directory.path_join(editor_run_gut_config_path)\n\t# Should this print a message or something instead?  Probably, but then I'd\n\t# be repeating even more code than if this was just a constant.  So I didn't,\n\t# even though I wanted to put make the message a easter eggish fun message.\n\t# I didn't, so this dumb comment will have to serve as the easter eggish fun.\n\tset(v): pass\n\n\nstatic var editor_run_bbcode_results_path = 'gut_editor.bbcode':\n\tget: return temp_directory.path_join(editor_run_bbcode_results_path)\n\tset(v): pass\n\n\nstatic var editor_run_json_results_path = 'gut_editor.json':\n\tget: return temp_directory.path_join(editor_run_json_results_path)\n\tset(v): pass\n\n\nstatic var editor_shortcuts_path = 'gut_editor_shortcuts.cfg' :\n\tget: return temp_directory.path_join(editor_shortcuts_path)\n\tset(v): pass\n\n\nstatic var _user_prefs = null\nstatic var user_prefs = _user_prefs :\n\t# workaround not being able to reference EditorInterface when not in\n\t# the editor.  This shouldn't be referenced by anything not in the\n\t# editor.\n\tget:\n\t\tif(_user_prefs == null and Engine.is_editor_hint()):\n\t\t\t_user_prefs = GutUserPreferences.new(EditorInterface.get_editor_settings())\n\t\treturn _user_prefs\n\n\nstatic func create_temp_directory():\n\tDirAccess.make_dir_recursive_absolute(temp_directory)\n\n"
  },
  {
    "path": "addons/gut/gui/editor_globals.gd.uid",
    "content": "uid://f5ak18bwmj26\n"
  },
  {
    "path": "addons/gut/gui/gut_config_gui.gd",
    "content": "var PanelControls = load(\"res://addons/gut/gui/panel_controls.gd\")\nvar GutConfig = load('res://addons/gut/gut_config.gd')\n\nconst DIRS_TO_LIST = 6\n\nvar _base_container = null\n# All the various PanelControls indexed by thier gut_config keys.\nvar _cfg_ctrls = {}\n\n# specific titles that we need to do stuff with\nvar _titles = {\n\tdirs = null\n}\n# All titles so we can free them when we want.\nvar _all_titles = []\n\n\nfunc _init(cont):\n\t_base_container = cont\n\n\nfunc _add_title(text):\n\tvar row = PanelControls.BaseGutPanelControl.new(text, text)\n\t_base_container.add_child(row)\n\trow.connect('draw', _on_title_cell_draw.bind(row))\n\t_all_titles.append(row)\n\treturn row\n\nfunc _add_ctrl(key, ctrl):\n\t_cfg_ctrls[key] = ctrl\n\t_base_container.add_child(ctrl)\n\n\nfunc _add_number(key, value, disp_text, v_min, v_max, hint=''):\n\tvar ctrl = PanelControls.NumberControl.new(disp_text, value, v_min, v_max, hint)\n\t_add_ctrl(key, ctrl)\n\treturn ctrl\n\n\nfunc _add_select(key, value, values, disp_text, hint=''):\n\tvar ctrl = PanelControls.SelectControl.new(disp_text, value, values, hint)\n\t_add_ctrl(key, ctrl)\n\treturn ctrl\n\n\nfunc _add_value(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.StringControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\treturn ctrl\n\n\nfunc _add_boolean(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.BooleanControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\treturn ctrl\n\n\nfunc _add_directory(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\tctrl.dialog.title = disp_text\n\treturn ctrl\n\n\nfunc _add_file(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\tctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_OPEN_FILE\n\tctrl.dialog.title = disp_text\n\treturn ctrl\n\n\nfunc _add_save_file_anywhere(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.DirectoryControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\tctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_SAVE_FILE\n\tctrl.dialog.access = ctrl.dialog.ACCESS_FILESYSTEM\n\tctrl.dialog.title = disp_text\n\treturn ctrl\n\n\nfunc _add_color(key, value, disp_text, hint=''):\n\tvar ctrl = PanelControls.ColorControl.new(disp_text, value, hint)\n\t_add_ctrl(key, ctrl)\n\treturn ctrl\n\n\nfunc _add_save_load():\n\tvar ctrl = PanelControls.SaveLoadControl.new('Config', '', '')\n\t_base_container.add_child(ctrl)\n\n\tctrl.save_path_chosen.connect(_on_save_path_chosen)\n\tctrl.load_path_chosen.connect(_on_load_path_chosen)\n\n\t_cfg_ctrls['save_load'] = ctrl\n\treturn ctrl\n\n# ------------------\n# Events\n# ------------------\nfunc _on_title_cell_draw(which):\n\twhich.draw_rect(Rect2(Vector2(0, 0), which.size), Color(0, 0, 0, .15))\n\n\nfunc _on_save_path_chosen(path):\n\tsave_file(path)\n\n\nfunc _on_load_path_chosen(path):\n\tload_file.bind(path).call_deferred()\n\n# ------------------\n# Public\n# ------------------\nfunc get_config_issues():\n\tvar to_return = []\n\tvar has_directory = false\n\n\tfor i in range(DIRS_TO_LIST):\n\t\tvar key = str('directory_', i)\n\t\tvar path = _cfg_ctrls[key].value\n\t\tif(path != null and path != ''):\n\t\t\thas_directory = true\n\t\t\tif(!DirAccess.dir_exists_absolute(path)):\n\t\t\t\t_cfg_ctrls[key].mark_invalid(true)\n\t\t\t\tto_return.append(str('Test directory ', path, ' does not exist.'))\n\t\t\telse:\n\t\t\t\t_cfg_ctrls[key].mark_invalid(false)\n\t\telse:\n\t\t\t_cfg_ctrls[key].mark_invalid(false)\n\n\tif(!has_directory):\n\t\tto_return.append('You do not have any directories set.')\n\t\t_titles.dirs.mark_invalid(true)\n\telse:\n\t\t_titles.dirs.mark_invalid(false)\n\n\tif(!_cfg_ctrls.suffix.value.ends_with('.gd')):\n\t\t_cfg_ctrls.suffix.mark_invalid(true)\n\t\tto_return.append(\"Script suffix must end in '.gd'\")\n\telse:\n\t\t_cfg_ctrls.suffix.mark_invalid(false)\n\n\treturn to_return\n\n\nfunc clear():\n\tfor key in _cfg_ctrls:\n\t\t_cfg_ctrls[key].free()\n\n\t_cfg_ctrls.clear()\n\n\tfor entry in _all_titles:\n\t\tentry.free()\n\n\t_all_titles.clear()\n\n\nfunc save_file(path):\n\tvar gcfg = GutConfig.new()\n\tgcfg.options = get_options({})\n\tgcfg.save_file(path)\n\n\n\nfunc load_file(path):\n\tvar gcfg = GutConfig.new()\n\tgcfg.load_options(path)\n\tclear()\n\tset_options(gcfg.options)\n\n\n# --------------\n# SUPER dumb but VERY fun hack to hide settings.  The various _add methods will\n# return what they add.  If you want to hide it, just assign the result to this.\n# YES, I could have just put .visible at the end, but I didn't think of that\n# until just now, and this was fun, non-permanent and the .visible at the end\n# isn't as obvious as hide_this =\n#\n# Also, we can't just skip adding the controls because other things are looking\n# for them and things start to blow up if you don't add them.\nvar hide_this = null :\n\tset(val):\n\t\tval.visible = false\n\n# --------------\n\nfunc set_options(opts):\n\tvar options = opts.duplicate()\n\n\t# _add_title('Save/Load')\n\t_add_save_load()\n\n\t_add_title(\"Settings\")\n\t_add_number(\"log_level\", options.log_level, \"Log Level\", 0, 3,\n\t\t\"Detail level for log messages.\\n\" + \\\n\t\t\"\\t0: Errors and failures only.\\n\" + \\\n\t\t\"\\t1: Adds all test names + warnings + info\\n\" + \\\n\t\t\"\\t2: Shows all asserts\\n\" + \\\n\t\t\"\\t3: Adds more stuff probably, maybe not.\")\n\t_add_boolean('ignore_pause', options.ignore_pause, 'Ignore Pause',\n\t\t\"Ignore calls to pause_before_teardown\")\n\t_add_boolean('hide_orphans', options.hide_orphans, 'Hide Orphans',\n\t\t'Do not display orphan counts in output.')\n\t_add_boolean('should_exit', options.should_exit, 'Exit on Finish',\n\t\t\"Exit when tests finished.\")\n\t_add_boolean('should_exit_on_success', options.should_exit_on_success, 'Exit on Success',\n\t\t\"Exit if there are no failures.  Does nothing if 'Exit on Finish' is enabled.\")\n\t_add_select('double_strategy', 'Script Only', ['Include Native', 'Script Only'], 'Double Strategy',\n\t\t'\"Include Native\" will include native methods in Doubles.  \"Script Only\" will not.  ' + \"\\n\" + \\\n\t\t'The native method override warning is disabled when creating Doubles.' + \"\\n\" + \\\n\t\t'This is the default, you can override this at the script level or when creating doubles.')\n\t_cfg_ctrls.double_strategy.value = GutUtils.get_enum_value(\n\t\toptions.double_strategy, GutUtils.DOUBLE_STRATEGY, GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY)\n\t_add_boolean('errors_cause_failure', !options.errors_do_not_cause_failure, 'Errors cause failures.',\n\t\t\"When GUT generates an error (not an engine error) it causes tests to fail.\")\n\n\n\t_add_title('Runner Appearance')\n\thide_this = _add_boolean(\"gut_on_top\", options.gut_on_top, \"On Top\",\n\t\t\"The GUT Runner appears above children added during tests.\")\n\t_add_number('opacity', options.opacity, 'Opacity', 0, 100,\n\t\t\"The opacity of GUT when tests are running.\")\n\thide_this = _add_boolean('should_maximize', options.should_maximize, 'Maximize',\n\t\t\"Maximize GUT when tests are being run.\")\n\t_add_boolean('compact_mode', options.compact_mode, 'Compact Mode',\n\t\t'The runner will be in compact mode.  This overrides Maximize.')\n\t_add_select('font_name', options.font_name, GutUtils.avail_fonts, 'Font',\n\t\t\"The font to use for text output in the Gut Runner.\")\n\t_add_number('font_size', options.font_size, 'Font Size', 5, 100,\n\t\t\"The font size for text output in the Gut Runner.\")\n\thide_this = _add_color('font_color', options.font_color, 'Font Color',\n\t\t\"The font color for text output in the Gut Runner.\")\n\t_add_color('background_color', options.background_color, 'Background Color',\n\t\t\"The background color for text output in the Gut Runner.\")\n\t_add_boolean('disable_colors', options.disable_colors, 'Disable Formatting',\n\t\t'Disable formatting and colors used in the Runner.  Does not affect panel output.')\n\n\n\t_titles.dirs = _add_title('Test Directories')\n\t_add_boolean('include_subdirs', options.include_subdirs, 'Include Subdirs',\n\t\t\"Include subdirectories of the directories configured below.\")\n\n\tvar dirs_to_load = options.configured_dirs\n\tif(options.dirs.size() > dirs_to_load.size()):\n\t\tdirs_to_load = options.dirs\n\n\tfor i in range(DIRS_TO_LIST):\n\t\tvar value = ''\n\t\tif(dirs_to_load.size() > i):\n\t\t\tvalue = dirs_to_load[i]\n\n\t\tvar test_dir = _add_directory(str('directory_', i), value, str(i))\n\t\ttest_dir.enabled_button.visible = true\n\t\ttest_dir.enabled_button.button_pressed = options.dirs.has(value)\n\n\n\t_add_title(\"XML Output\")\n\t_add_save_file_anywhere(\"junit_xml_file\", options.junit_xml_file, \"Output Path\",\n\t\t\"Path3D and filename where GUT should create a JUnit compliant XML file.  \" +\n\t\t\"This file will contain the results of the last test run.  To avoid \" +\n\t\t\"overriding the file use Include Timestamp.\")\n\t_add_boolean(\"junit_xml_timestamp\", options.junit_xml_timestamp, \"Include Timestamp\",\n\t\t\"Include a timestamp in the filename so that each run gets its own xml file.\")\n\n\n\t_add_title('Hooks')\n\t_add_file('pre_run_script', options.pre_run_script, 'Pre-Run Hook',\n\t\t'This script will be run by GUT before any tests are run.')\n\t_add_file('post_run_script', options.post_run_script, 'Post-Run Hook',\n\t\t'This script will be run by GUT after all tests are run.')\n\n\n\t_add_title('Misc')\n\t_add_value('prefix', options.prefix, 'Script Prefix',\n\t\t\"The filename prefix for all test scripts.\")\n\t_add_value('suffix', options.suffix, 'Script Suffix',\n\t\t\"Script suffix, including .gd extension.  For example '_foo.gd'.\")\n\t_add_number('paint_after', options.paint_after, 'Paint After', 0.0, 1.0,\n\t\t\"How long GUT will wait before pausing for 1 frame to paint the screen.  0 is never.\")\n\n\t# since _add_number doesn't set step property, it will default to a step of\n\t# 1 and cast values to int.  Give it a .5 step and re-set the value.\n\t_cfg_ctrls.paint_after.value_ctrl.step = .05\n\t_cfg_ctrls.paint_after.value = options.paint_after\n\n\n\nfunc get_options(base_opts):\n\tvar to_return = base_opts.duplicate()\n\n\t# Settings\n\tto_return.log_level = _cfg_ctrls.log_level.value\n\tto_return.ignore_pause = _cfg_ctrls.ignore_pause.value\n\tto_return.hide_orphans = _cfg_ctrls.hide_orphans.value\n\tto_return.should_exit = _cfg_ctrls.should_exit.value\n\tto_return.should_exit_on_success = _cfg_ctrls.should_exit_on_success.value\n\tto_return.double_strategy = _cfg_ctrls.double_strategy.value\n\tto_return.errors_do_not_cause_failure = !_cfg_ctrls.errors_cause_failure.value\n\n\n\t# Runner Appearance\n\tto_return.font_name = _cfg_ctrls.font_name.text\n\tto_return.font_size = _cfg_ctrls.font_size.value\n\tto_return.should_maximize = _cfg_ctrls.should_maximize.value\n\tto_return.compact_mode = _cfg_ctrls.compact_mode.value\n\tto_return.opacity = _cfg_ctrls.opacity.value\n\tto_return.background_color = _cfg_ctrls.background_color.value.to_html()\n\tto_return.font_color = _cfg_ctrls.font_color.value.to_html()\n\tto_return.disable_colors = _cfg_ctrls.disable_colors.value\n\tto_return.gut_on_top = _cfg_ctrls.gut_on_top.value\n\tto_return.paint_after = _cfg_ctrls.paint_after.value\n\n\n\t# Directories\n\tto_return.include_subdirs = _cfg_ctrls.include_subdirs.value\n\tvar dirs = []\n\tvar configured_dirs = []\n\tfor i in range(DIRS_TO_LIST):\n\t\tvar key = str('directory_', i)\n\t\tvar ctrl = _cfg_ctrls[key]\n\t\tif(ctrl.value != '' and ctrl.value != null):\n\t\t\tconfigured_dirs.append(ctrl.value)\n\t\t\tif(ctrl.enabled_button.button_pressed):\n\t\t\t\tdirs.append(ctrl.value)\n\tto_return.dirs = dirs\n\tto_return.configured_dirs = configured_dirs\n\n\t# XML Output\n\tto_return.junit_xml_file = _cfg_ctrls.junit_xml_file.value\n\tto_return.junit_xml_timestamp = _cfg_ctrls.junit_xml_timestamp.value\n\n\t# Hooks\n\tto_return.pre_run_script = _cfg_ctrls.pre_run_script.value\n\tto_return.post_run_script = _cfg_ctrls.post_run_script.value\n\n\t# Misc\n\tto_return.prefix = _cfg_ctrls.prefix.value\n\tto_return.suffix = _cfg_ctrls.suffix.value\n\n\treturn to_return\n\n\nfunc mark_saved():\n\tfor key in _cfg_ctrls:\n\t\t_cfg_ctrls[key].mark_unsaved(false)\n"
  },
  {
    "path": "addons/gut/gui/gut_config_gui.gd.uid",
    "content": "uid://brrhd3umxj5ua\n"
  },
  {
    "path": "addons/gut/gui/gut_gui.gd",
    "content": "extends Control\n# ##############################################################################\n# This is the decoupled GUI for gut.gd\n#\n# This is a \"generic\" interface between a GUI and gut.gd.  It assumes there are\n# certain controls with specific names.  It will then interact with those\n# controls based on signals emitted from gut.gd in order to give the user\n# feedback about the progress of the test run and the results.\n#\n# Optional controls are marked as such in the _ctrls dictionary.  The names\n# of the controls can be found in _populate_ctrls.\n# ##############################################################################\nvar _gut = null\n\nvar _ctrls = {\n\tbtn_continue = null,\n\tpath_dir = null,\n\tpath_file = null,\n\tprog_script = null,\n\tprog_test = null,\n\trtl = null,                 # optional\n\trtl_bg = null,              # required if rtl exists\n\tswitch_modes = null,\n\ttime_label = null,\n\ttitle = null,\n\ttitle_bar = null,\n}\n\nvar _title_mouse = {\n\tdown = false\n}\n\n\nsignal switch_modes()\n\nvar _max_position = Vector2(100, 100)\n\nfunc _ready():\n\t_populate_ctrls()\n\n\t_ctrls.btn_continue.visible = false\n\t_ctrls.btn_continue.pressed.connect(_on_continue_pressed)\n\t_ctrls.switch_modes.pressed.connect(_on_switch_modes_pressed)\n\t_ctrls.title_bar.gui_input.connect(_on_title_bar_input)\n\n\t_ctrls.prog_script.value = 0\n\t_ctrls.prog_test.value = 0\n\t_ctrls.path_dir.text = ''\n\t_ctrls.path_file.text = ''\n\t_ctrls.time_label.text = ''\n\n\t_max_position = get_display_size() - Vector2(30, _ctrls.title_bar.size.y)\n\n\nfunc _process(_delta):\n\tif(_gut != null and _gut.is_running()):\n\t\tset_elapsed_time(_gut.get_elapsed_time())\n\n\n# ------------------\n# Private\n# ------------------\nfunc get_display_size():\n\treturn get_viewport().get_visible_rect().size\n\n\nfunc _populate_ctrls():\n\t# Brute force, but flexible.  This allows for all the controls to exist\n\t# anywhere, and as long as they all have the right name, they will be\n\t# found.\n\t_ctrls.btn_continue = _get_first_child_named('Continue', self)\n\t_ctrls.path_dir = _get_first_child_named('Path', self)\n\t_ctrls.path_file = _get_first_child_named('File', self)\n\t_ctrls.prog_script = _get_first_child_named('ProgressScript', self)\n\t_ctrls.prog_test = _get_first_child_named('ProgressTest', self)\n\t_ctrls.rtl = _get_first_child_named('TestOutput', self)\n\t_ctrls.rtl_bg = _get_first_child_named('OutputBG', self)\n\t_ctrls.switch_modes = _get_first_child_named(\"SwitchModes\", self)\n\t_ctrls.time_label = _get_first_child_named('TimeLabel', self)\n\t_ctrls.title = _get_first_child_named(\"Title\", self)\n\t_ctrls.title_bar = _get_first_child_named(\"TitleBar\", self)\n\n\nfunc _get_first_child_named(obj_name, parent_obj):\n\tif(parent_obj == null):\n\t\treturn null\n\n\tvar kids = parent_obj.get_children()\n\tvar index = 0\n\tvar to_return = null\n\n\twhile(index < kids.size() and to_return == null):\n\t\tif(str(kids[index]).find(str(obj_name, ':')) != -1):\n\t\t\tto_return = kids[index]\n\t\telse:\n\t\t\tto_return = _get_first_child_named(obj_name, kids[index])\n\t\t\tif(to_return == null):\n\t\t\t\tindex += 1\n\n\treturn to_return\n\n\n\n# ------------------\n# Events\n# ------------------\nfunc _on_title_bar_input(event : InputEvent):\n\tif(event is InputEventMouseMotion):\n\t\tif(_title_mouse.down):\n\t\t\tposition += event.relative\n\t\t\tposition.x = clamp(position.x, 0, _max_position.x)\n\t\t\tposition.y = clamp(position.y, 0, _max_position.y)\n\telif(event is InputEventMouseButton):\n\t\tif(event.button_index == MOUSE_BUTTON_LEFT):\n\t\t\t_title_mouse.down = event.pressed\n\n\nfunc _on_continue_pressed():\n\t_gut.end_teardown_pause()\n\n\nfunc _on_gut_start_run():\n\tif(_ctrls.rtl != null):\n\t\t_ctrls.rtl.clear()\n\tset_num_scripts(_gut.get_test_collector().scripts.size())\n\n\nfunc _on_gut_end_run():\n\t_ctrls.prog_test.value = _ctrls.prog_test.max_value\n\t_ctrls.prog_script.value = _ctrls.prog_script.max_value\n\n\nfunc _on_gut_start_script(script_obj):\n\tnext_script(script_obj.get_full_name(), script_obj.tests.size())\n\n\nfunc _on_gut_end_script():\n\tpass\n\n\nfunc _on_gut_start_test(test_name):\n\tnext_test(test_name)\n\n\nfunc _on_gut_end_test():\n\tpass\n\n\nfunc _on_gut_start_pause():\n\tpause_before_teardown()\n\n\nfunc _on_gut_end_pause():\n\t_ctrls.btn_continue.visible = false\n\n\nfunc _on_switch_modes_pressed():\n\tswitch_modes.emit()\n\n# ------------------\n# Public\n# ------------------\nfunc set_num_scripts(val):\n\t_ctrls.prog_script.value = 0\n\t_ctrls.prog_script.max_value = val\n\n\nfunc next_script(path, num_tests):\n\t_ctrls.prog_script.value += 1\n\t_ctrls.prog_test.value = 0\n\t_ctrls.prog_test.max_value = num_tests\n\n\t_ctrls.path_dir.text = path.get_base_dir()\n\t_ctrls.path_file.text = path.get_file()\n\n\nfunc next_test(__test_name):\n\t_ctrls.prog_test.value += 1\n\n\nfunc pause_before_teardown():\n\t_ctrls.btn_continue.visible = true\n\n\nfunc set_gut(g):\n\tif(_gut == g):\n\t\treturn\n\t_gut = g\n\tg.start_run.connect(_on_gut_start_run)\n\tg.end_run.connect(_on_gut_end_run)\n\n\tg.start_script.connect(_on_gut_start_script)\n\tg.end_script.connect(_on_gut_end_script)\n\n\tg.start_test.connect(_on_gut_start_test)\n\tg.end_test.connect(_on_gut_end_test)\n\n\tg.start_pause_before_teardown.connect(_on_gut_start_pause)\n\tg.end_pause_before_teardown.connect(_on_gut_end_pause)\n\nfunc get_gut():\n\treturn _gut\n\nfunc get_textbox():\n\treturn _ctrls.rtl\n\nfunc set_elapsed_time(t):\n\t_ctrls.time_label.text = str(\"%6.1f\" % t, 's')\n\n\nfunc set_bg_color(c):\n\t_ctrls.rtl_bg.color = c\n\n\nfunc set_title(text):\n\t_ctrls.title.text = text\n\n\nfunc to_top_left():\n\tself.position = Vector2(5, 5)\n\n\nfunc to_bottom_right():\n\tvar win_size = get_display_size()\n\tself.position = win_size - Vector2(self.size) - Vector2(5, 5)\n\n\nfunc align_right():\n\tvar win_size = get_display_size()\n\tself.position.x = win_size.x - self.size.x -5\n\tself.position.y = 5\n\tself.size.y = win_size.y - 10\n"
  },
  {
    "path": "addons/gut/gui/gut_gui.gd.uid",
    "content": "uid://duu660pjff8cv\n"
  },
  {
    "path": "addons/gut/gui/gut_user_preferences.gd",
    "content": "class GutEditorPref:\n\tvar gut_pref_prefix = 'gut/'\n\tvar pname = '__not_set__'\n\tvar default = null\n\tvar value = '__not_set__'\n\tvar _settings = null\n\n\tfunc _init(n, d, s):\n\t\tpname = n\n\t\tdefault = d\n\t\t_settings = s\n\t\tload_it()\n\n\tfunc _prefstr():\n\t\tvar to_return = str(gut_pref_prefix, pname)\n\t\treturn to_return\n\n\tfunc save_it():\n\t\t_settings.set_setting(_prefstr(), value)\n\n\tfunc load_it():\n\t\tif(_settings.has_setting(_prefstr())):\n\t\t\tvalue = _settings.get_setting(_prefstr())\n\t\telse:\n\t\t\tvalue = default\n\n\tfunc erase():\n\t\t_settings.erase(_prefstr())\n\n\nconst EMPTY = '-- NOT_SET --'\n\n# -- Editor ONLY Settings --\nvar output_font_name = null\nvar output_font_size = null\nvar hide_result_tree = null\nvar hide_output_text = null\nvar hide_settings = null\nvar use_colors = null\t# ? might be output panel\n\n# var shortcut_run_all = null\n# var shortcut_run_current_script = null\n# var shortcut_run_current_inner = null\n# var shortcut_run_current_test = null\n# var shortcut_panel_button = null\n\n\nfunc _init(editor_settings):\n\toutput_font_name = GutEditorPref.new('output_font_name', 'CourierPrime', editor_settings)\n\toutput_font_size = GutEditorPref.new('output_font_size', 30, editor_settings)\n\thide_result_tree = GutEditorPref.new('hide_result_tree', false, editor_settings)\n\thide_output_text = GutEditorPref.new('hide_output_text', false, editor_settings)\n\thide_settings = GutEditorPref.new('hide_settings', false, editor_settings)\n\tuse_colors = GutEditorPref.new('use_colors', true, editor_settings)\n\n\t# shortcut_run_all = GutEditorPref.new('shortcut_run_all', EMPTY, editor_settings)\n\t# shortcut_run_current_script = GutEditorPref.new('shortcut_run_current_script', EMPTY, editor_settings)\n\t# shortcut_run_current_inner = GutEditorPref.new('shortcut_run_current_inner', EMPTY, editor_settings)\n\t# shortcut_run_current_test = GutEditorPref.new('shortcut_run_current_test', EMPTY, editor_settings)\n\t# shortcut_panel_button = GutEditorPref.new('shortcut_panel_button', EMPTY, editor_settings)\n\nfunc save_it():\n\tfor prop in get_property_list():\n\t\tvar val = get(prop.name)\n\t\tif(val is GutEditorPref):\n\t\t\tval.save_it()\n\n\nfunc load_it():\n\tfor prop in get_property_list():\n\t\tvar val = get(prop.name)\n\t\tif(val is GutEditorPref):\n\t\t\tval.load_it()\n\n\nfunc erase_all():\n\tfor prop in get_property_list():\n\t\tvar val = get(prop.name)\n\t\tif(val is GutEditorPref):\n\t\t\tval.erase()"
  },
  {
    "path": "addons/gut/gui/gut_user_preferences.gd.uid",
    "content": "uid://ce34uxa87kx45\n"
  },
  {
    "path": "addons/gut/gui/panel_controls.gd",
    "content": "# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass BaseGutPanelControl:\n\textends HBoxContainer\n\tvar label = Label.new()\n\tvar _lbl_unsaved = Label.new()\n\tvar _lbl_invalid = Label.new()\n\n\tvar value = null:\n\t\tget: return get_value()\n\t\tset(val): set_value(val)\n\n\tsignal changed\n\n\tfunc _init(title, val, hint=\"\"):\n\t\tsize_flags_horizontal = SIZE_EXPAND_FILL\n\t\tmouse_filter = MOUSE_FILTER_PASS\n\n\t\tlabel.size_flags_horizontal = label.SIZE_EXPAND_FILL\n\t\tlabel.mouse_filter = label.MOUSE_FILTER_STOP\n\t\tadd_child(label)\n\n\t\t_lbl_unsaved.text = '*'\n\t\t_lbl_unsaved.visible = false\n\t\tadd_child(_lbl_unsaved)\n\n\t\t_lbl_invalid.text = '!'\n\t\t_lbl_invalid.visible = false\n\t\tadd_child(_lbl_invalid)\n\n\t\tlabel.text = title\n\t\tlabel.tooltip_text = hint\n\n\n\tfunc mark_unsaved(is_it=true):\n\t\t_lbl_unsaved.visible = is_it\n\n\n\tfunc mark_invalid(is_it):\n\t\t_lbl_invalid.visible = is_it\n\n\t# -- Virtual --\n\t#\n\t# value_ctrl (all should declare the value_ctrl)\n\t#\n\tfunc set_value(value):\n\t\tpass\n\n\tfunc get_value():\n\t\tpass\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass NumberControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl = SpinBox.new()\n\n\tfunc _init(title, val, v_min, v_max, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\n\t\tvalue_ctrl.value = val\n\t\tvalue_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL\n\t\tvalue_ctrl.min_value = v_min\n\t\tvalue_ctrl.max_value = v_max\n\t\tvalue_ctrl.value_changed.connect(_on_value_changed)\n\t\tvalue_ctrl.select_all_on_focus = true\n\t\tadd_child(value_ctrl)\n\n\tfunc _on_value_changed(new_value):\n\t\tchanged.emit()\n\n\tfunc get_value():\n\t\treturn value_ctrl.value\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.value = val\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass StringControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl = LineEdit.new()\n\n\tfunc _init(title, val, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\n\t\tvalue_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL\n\t\tvalue_ctrl.text = val\n\t\tvalue_ctrl.text_changed.connect(_on_text_changed)\n\t\tvalue_ctrl.select_all_on_focus = true\n\t\tadd_child(value_ctrl)\n\n\tfunc _on_text_changed(new_value):\n\t\tchanged.emit()\n\n\tfunc get_value():\n\t\treturn value_ctrl.text\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.text = val\n\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass BooleanControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl = CheckBox.new()\n\n\tfunc _init(title, val, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\n\t\tvalue_ctrl.button_pressed = val\n\t\tvalue_ctrl.toggled.connect(_on_button_toggled)\n\t\tadd_child(value_ctrl)\n\n\tfunc _on_button_toggled(new_value):\n\t\tchanged.emit()\n\n\tfunc get_value():\n\t\treturn value_ctrl.button_pressed\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.button_pressed = val\n\n\n# ------------------------------------------------------------------------------\n# value is \"selected\" and is gettable and settable\n# text is the text value of the selected item, it is gettable only\n# ------------------------------------------------------------------------------\nclass SelectControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl = OptionButton.new()\n\n\tvar text = '' :\n\t\tget: return value_ctrl.get_item_text(value_ctrl.selected)\n\t\tset(val): pass\n\n\tfunc _init(title, val, choices, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\n\t\tvar select_idx = 0\n\t\tfor i in range(choices.size()):\n\t\t\tvalue_ctrl.add_item(choices[i])\n\t\t\tif(val == choices[i]):\n\t\t\t\tselect_idx = i\n\t\tvalue_ctrl.selected = select_idx\n\t\tvalue_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL\n\t\tvalue_ctrl.item_selected.connect(_on_item_selected)\n\t\tadd_child(value_ctrl)\n\n\tfunc _on_item_selected(idx):\n\t\tchanged.emit()\n\n\tfunc get_value():\n\t\treturn value_ctrl.selected\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.selected = val\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass ColorControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl = ColorPickerButton.new()\n\n\tfunc _init(title, val, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\t\tvalue_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL\n\t\tvalue_ctrl.color = val\n\t\tadd_child(value_ctrl)\n\n\tfunc get_value():\n\t\treturn value_ctrl.color\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.color = val\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass DirectoryControl:\n\textends BaseGutPanelControl\n\n\tvar value_ctrl := LineEdit.new()\n\tvar dialog := FileDialog.new()\n\tvar enabled_button = CheckButton.new()\n\n\tvar _btn_dir := Button.new()\n\n\tfunc _init(title, val, hint=\"\"):\n\t\tsuper._init(title, val, hint)\n\n\t\tlabel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN\n\n\t\t_btn_dir.text = '...'\n\t\t_btn_dir.pressed.connect(_on_dir_button_pressed)\n\n\t\tvalue_ctrl.text = val\n\t\tvalue_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL\n\t\tvalue_ctrl.select_all_on_focus = true\n\t\tvalue_ctrl.text_changed.connect(_on_value_changed)\n\n\t\tdialog.file_mode = dialog.FILE_MODE_OPEN_DIR\n\t\tdialog.unresizable = false\n\t\tdialog.dir_selected.connect(_on_selected)\n\t\tdialog.file_selected.connect(_on_selected)\n\n\t\tenabled_button.button_pressed = true\n\t\tenabled_button.visible = false\n\n\t\tadd_child(enabled_button)\n\t\tadd_child(value_ctrl)\n\t\tadd_child(_btn_dir)\n\t\tadd_child(dialog)\n\n\tfunc _update_display():\n\t\tvar is_empty = value_ctrl.text == ''\n\t\tenabled_button.button_pressed = !is_empty\n\t\tenabled_button.disabled = is_empty\n\n\n\tfunc _ready():\n\t\tif(Engine.is_editor_hint()):\n\t\t\tdialog.size = Vector2(1000, 700)\n\t\telse:\n\t\t\tdialog.size = Vector2(500, 350)\n\t\t_update_display()\n\n\tfunc _on_value_changed(new_text):\n\t\t_update_display()\n\n\tfunc _on_selected(path):\n\t\tvalue_ctrl.text = path\n\t\t_update_display()\n\n\tfunc _on_dir_button_pressed():\n\t\tdialog.current_dir = value_ctrl.text\n\t\tdialog.popup_centered()\n\n\tfunc get_value():\n\t\treturn value_ctrl.text\n\n\tfunc set_value(val):\n\t\tvalue_ctrl.text = val\n\n\n# ------------------------------------------------------------------------------\n# Features:\n# \tButtons to pick res://, user://, or anywhere on the OS.\n# ------------------------------------------------------------------------------\nclass FileDialogSuperPlus:\n\textends FileDialog\n\n\tvar show_diretory_types = true :\n\t\tset(val) :\n\t\t\tshow_diretory_types = val\n\t\t\t_update_display()\n\n\tvar show_res = true :\n\t\tset(val) :\n\t\t\tshow_res = val\n\t\t\t_update_display()\n\n\tvar show_user = true :\n\t\tset(val) :\n\t\t\tshow_user = val\n\t\t\t_update_display()\n\n\tvar show_os = true :\n\t\tset(val) :\n\t\t\tshow_os = val\n\t\t\t_update_display()\n\n\tvar _dir_type_hbox = null\n\tvar _btn_res = null\n\tvar _btn_user = null\n\tvar _btn_os = null\n\n\tfunc _ready():\n\t\t_init_controls()\n\t\t_update_display()\n\n\n\tfunc _init_controls():\n\t\t_dir_type_hbox = HBoxContainer.new()\n\n\t\t_btn_res = Button.new()\n\t\t_btn_user = Button.new()\n\t\t_btn_os = Button.new()\n\t\tvar spacer1 = CenterContainer.new()\n\t\tspacer1.size_flags_horizontal = spacer1.SIZE_EXPAND_FILL\n\t\tvar spacer2 = spacer1.duplicate()\n\n\t\t_dir_type_hbox.add_child(spacer1)\n\t\t_dir_type_hbox.add_child(_btn_res)\n\t\t_dir_type_hbox.add_child(_btn_user)\n\t\t_dir_type_hbox.add_child(_btn_os)\n\t\t_dir_type_hbox.add_child(spacer2)\n\n\t\t_btn_res.text = 'res://'\n\t\t_btn_user.text = 'user://'\n\t\t_btn_os.text = '  OS  '\n\n\t\tget_vbox().add_child(_dir_type_hbox)\n\t\tget_vbox().move_child(_dir_type_hbox, 0)\n\n\t\t_btn_res.pressed.connect(func(): access = ACCESS_RESOURCES)\n\t\t_btn_user.pressed.connect(func(): access = ACCESS_USERDATA)\n\t\t_btn_os.pressed.connect(func(): access = ACCESS_FILESYSTEM)\n\n\n\tfunc _update_display():\n\t\tif(is_inside_tree()):\n\t\t\t_dir_type_hbox.visible = show_diretory_types\n\t\t\t_btn_res.visible = show_res\n\t\t\t_btn_user.visible = show_user\n\t\t\t_btn_os.visible = show_os\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass SaveLoadControl:\n\textends BaseGutPanelControl\n\n\tvar btn_load = Button.new()\n\tvar btn_save = Button.new()\n\n\tvar dlg_load := FileDialogSuperPlus.new()\n\tvar dlg_save := FileDialogSuperPlus.new()\n\n\tsignal save_path_chosen(path)\n\tsignal load_path_chosen(path)\n\n\tfunc _init(title, val, hint):\n\t\tsuper._init(title, val, hint)\n\n\t\tbtn_load.text = \"Load\"\n\t\tbtn_load.custom_minimum_size.x = 100\n\t\tbtn_load.pressed.connect(_on_load_pressed)\n\t\tadd_child(btn_load)\n\n\t\tbtn_save.text = \"Save As\"\n\t\tbtn_save.custom_minimum_size.x = 100\n\t\tbtn_save.pressed.connect(_on_save_pressed)\n\t\tadd_child(btn_save)\n\n\t\tdlg_load.file_mode = dlg_load.FILE_MODE_OPEN_FILE\n\t\tdlg_load.unresizable = false\n\t\tdlg_load.dir_selected.connect(_on_load_selected)\n\t\tdlg_load.file_selected.connect(_on_load_selected)\n\t\tadd_child(dlg_load)\n\n\t\tdlg_save.file_mode = dlg_save.FILE_MODE_SAVE_FILE\n\t\tdlg_save.unresizable = false\n\t\tdlg_save.dir_selected.connect(_on_save_selected)\n\t\tdlg_save.file_selected.connect(_on_save_selected)\n\t\tadd_child(dlg_save)\n\n\n\tfunc _ready():\n\t\tif(Engine.is_editor_hint()):\n\t\t\tdlg_load.size = Vector2(1000, 700)\n\t\t\tdlg_save.size = Vector2(1000, 700)\n\t\telse:\n\t\t\tdlg_load.size = Vector2(500, 350)\n\t\t\tdlg_save.size = Vector2(500, 350)\n\n\tfunc _on_load_selected(path):\n\t\tload_path_chosen.emit(path)\n\n\tfunc _on_save_selected(path):\n\t\tsave_path_chosen.emit(path)\n\n\tfunc _on_load_pressed():\n\t\tdlg_load.popup_centered()\n\n\tfunc _on_save_pressed():\n\t\tdlg_save.popup_centered()\n\n# ------------------------------------------------------------------------------\n# This one was never used in gut_config_gui...but I put some work into it and\n# I'm a sucker for that kinda thing.  Delete this when you get tired of looking\n# at it.\n# ------------------------------------------------------------------------------\n# class Vector2Ctrl:\n# \textends VBoxContainer\n\n# \tvar value = Vector2(-1, -1) :\n# \t\tget:\n# \t\t\treturn get_value()\n# \t\tset(val):\n# \t\t\tset_value(val)\n# \tvar disabled = false :\n# \t\tget:\n# \t\t\treturn get_disabled()\n# \t\tset(val):\n# \t\t\tset_disabled(val)\n# \tvar x_spin = SpinBox.new()\n# \tvar y_spin = SpinBox.new()\n\n# \tfunc _init():\n# \t\tadd_child(_make_one('x:  ', x_spin))\n# \t\tadd_child(_make_one('y:  ', y_spin))\n\n# \tfunc _make_one(txt, spinner):\n# \t\tvar hbox = HBoxContainer.new()\n# \t\tvar lbl = Label.new()\n# \t\tlbl.text = txt\n# \t\thbox.add_child(lbl)\n# \t\thbox.add_child(spinner)\n# \t\tspinner.min_value = -1\n# \t\tspinner.max_value = 10000\n# \t\tspinner.size_flags_horizontal = spinner.SIZE_EXPAND_FILL\n# \t\treturn hbox\n\n# \tfunc set_value(v):\n# \t\tif(v != null):\n# \t\t\tx_spin.value = v[0]\n# \t\t\ty_spin.value = v[1]\n\n# \t# Returns array instead of vector2 b/c that is what is stored in\n# \t# in the dictionary and what is expected everywhere else.\n# \tfunc get_value():\n# \t\treturn [x_spin.value, y_spin.value]\n\n# \tfunc set_disabled(should):\n# \t\tget_parent().visible = !should\n# \t\tx_spin.visible = !should\n# \t\ty_spin.visible = !should\n\n# \tfunc get_disabled():\n# \t\tpass\n"
  },
  {
    "path": "addons/gut/gui/panel_controls.gd.uid",
    "content": "uid://bqf8edf4yh5l2\n"
  },
  {
    "path": "addons/gut/gui/play.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cr6tvdv0ve6cv\"\npath=\"res://.godot/imported/play.png-5c90e88e8136487a183a099d67a7de24.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/gui/play.png\"\ndest_files=[\"res://.godot/imported/play.png-5c90e88e8136487a183a099d67a7de24.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/gui/script_text_editor_controls.gd",
    "content": "# Holds weakrefs to a ScriptTextEditor and related children nodes\n# that might be useful.  Though the CodeEdit is really the only one, but\n# since the tree may change, the first TextEdit under a CodeTextEditor is\n# the one we use...so we hold a ref to the CodeTextEditor too.\nclass ScriptEditorControlRef:\n\tvar _text_edit = null\n\tvar _script_editor = null\n\tvar _code_editor = null\n\n\tfunc _init(script_edit):\n\t\t_script_editor = weakref(script_edit)\n\t\t_populate_controls()\n\t\t# print(\"_script_editor = \", script_edit, ' vis = ', is_visible())\n\n\n\tfunc _populate_controls():\n\t\t# who knows if the tree will change so get the first instance of each\n\t\t# type of control we care about.  Chances are there won't be more than\n\t\t# one of these in the future, but their position in the tree may change.\n\t\t_code_editor = weakref(_get_first_child_named('CodeTextEditor', _script_editor.get_ref()))\n\t\t_text_edit = weakref(_get_first_child_named(\"CodeEdit\", _code_editor.get_ref()))\n\n\n\tfunc _get_first_child_named(obj_name, parent_obj):\n\t\tif(parent_obj == null):\n\t\t\treturn null\n\n\t\tvar kids = parent_obj.get_children()\n\t\tvar index = 0\n\t\tvar to_return = null\n\n\t\twhile(index < kids.size() and to_return == null):\n\t\t\tif(str(kids[index]).find(str(\"<\", obj_name)) != -1):\n\t\t\t\tto_return = kids[index]\n\t\t\telse:\n\t\t\t\tto_return = _get_first_child_named(obj_name, kids[index])\n\t\t\t\tif(to_return == null):\n\t\t\t\t\tindex += 1\n\n\t\treturn to_return\n\n\n\tfunc get_script_text_edit():\n\t\treturn _script_editor.get_ref()\n\n\n\tfunc get_text_edit():\n\t\t# ScriptTextEditors that are loaded when the project is opened\n\t\t# do not have their children populated yet.  So if we may have to\n\t\t# _populate_controls again if we don't have one.\n\t\tif(_text_edit == null):\n\t\t\t_populate_controls()\n\t\treturn _text_edit.get_ref()\n\n\n\tfunc get_script_editor():\n\t\treturn _script_editor\n\n\n\tfunc is_visible():\n\t\tvar to_return = false\n\t\tif(_script_editor.get_ref()):\n\t\t\tto_return = _script_editor.get_ref().visible\n\t\treturn to_return\n\n# ##############################################################################\n#\n# ##############################################################################\n\n# Used to make searching for the controls easier and faster.\nvar _script_editors_parent = null\n# reference the ScriptEditor instance\nvar _script_editor = null\n# Array of ScriptEditorControlRef containing all the opened ScriptTextEditors\n# and related controls at the time of the last refresh.\nvar _script_editor_controls = []\n\nvar _method_prefix = 'test_'\nvar _inner_class_prefix = 'Test'\n\nfunc _init(script_edit):\n\t_script_editor = script_edit\n\trefresh()\n\n\nfunc _is_script_editor(obj):\n\treturn str(obj).find('<ScriptTextEditor') != -1\n\n\n# Find the first ScriptTextEditor and then get its parent.  Done this way\n# because who knows if the parent object will change.  This is somewhat\n# future proofed.\nfunc _find_script_editors_parent():\n\tvar _first_editor = _get_first_child_of_type_name(\"ScriptTextEditor\", _script_editor)\n\tif(_first_editor != null):\n\t\t_script_editors_parent = _first_editor.get_parent()\n\n\nfunc _populate_editors():\n\tif(_script_editors_parent == null):\n\t\treturn\n\n\t_script_editor_controls.clear()\n\tfor child in _script_editors_parent.get_children():\n\t\tif(_is_script_editor(child)):\n\t\t\tvar ref = ScriptEditorControlRef.new(child)\n\t\t\t_script_editor_controls.append(ref)\n\n# Yes, this is the same as the one above but with a different name.  This was\n# easier than trying to find a place where it could be used by both.\nfunc _get_first_child_of_type_name(obj_name, parent_obj):\n\tif(parent_obj == null):\n\t\t# print('aborting search for ', obj_name, ' parent is null')\n\t\treturn null\n\n\tvar kids = parent_obj.get_children()\n\tvar index = 0\n\tvar to_return = null\n\n\tvar search_for = str(\"<\", obj_name)\n\t# print('searching for ', search_for, ' in ', parent_obj, ' kids ', kids.size())\n\twhile(index < kids.size() and to_return == null):\n\t\tvar this_one = str(kids[index])\n\t\t# print(search_for, ' :: ', this_one)\n\t\tif(this_one.find(search_for) != -1):\n\t\t\tto_return = kids[index]\n\t\telse:\n\t\t\tto_return = _get_first_child_of_type_name(obj_name, kids[index])\n\t\t\tif(to_return == null):\n\t\t\t\tindex += 1\n\n\treturn to_return\n\n\nfunc _get_func_name_from_line(text):\n\ttext = text.strip_edges()\n\tvar left = text.split(\"(\")[0]\n\tvar func_name = left.split(\" \")[1]\n\treturn func_name\n\n\nfunc _get_class_name_from_line(text):\n\ttext = text.strip_edges()\n\tvar right = text.split(\" \")[1]\n\tvar the_name = right.rstrip(\":\")\n\treturn the_name\n\nfunc refresh():\n\tif(_script_editors_parent == null):\n\t\t_find_script_editors_parent()\n\t\t# print(\"script editors parent = \", _script_editors_parent)\n\n\tif(_script_editors_parent != null):\n\t\t_populate_editors()\n\n\t# print(\"script editor controls = \", _script_editor_controls)\n\n\nfunc get_current_text_edit():\n\tvar cur_script_editor = null\n\tvar idx = 0\n\n\twhile(idx < _script_editor_controls.size() and cur_script_editor == null):\n\t\tif(_script_editor_controls[idx].is_visible()):\n\t\t\tcur_script_editor = _script_editor_controls[idx]\n\t\telse:\n\t\t\tidx += 1\n\n\tvar to_return = null\n\tif(cur_script_editor != null):\n\t\tto_return = cur_script_editor.get_text_edit()\n\n\treturn to_return\n\n\nfunc get_script_editor_controls():\n\tvar to_return = []\n\tfor ctrl_ref in _script_editor_controls:\n\t\tto_return.append(ctrl_ref.get_script_text_edit())\n\n\treturn to_return\n\n\nfunc get_line_info():\n\tvar editor = get_current_text_edit()\n\tif(editor == null):\n\t\treturn\n\n\tvar info = {\n\t\tscript = null,\n\t\tinner_class = null,\n\t\ttest_method = null\n\t}\n\n\tvar line = editor.get_caret_line()\n\tvar done_func = false\n\tvar done_inner = false\n\twhile(line > 0 and (!done_func or !done_inner)):\n\t\tif(editor.can_fold_line(line)):\n\t\t\tvar text = editor.get_line(line)\n\t\t\tvar strip_text = text.strip_edges(true, false) # only left\n\n\t\t\tif(!done_func and strip_text.begins_with(\"func \")):\n\t\t\t\tvar func_name = _get_func_name_from_line(text)\n\t\t\t\tif(func_name.begins_with(_method_prefix)):\n\t\t\t\t\tinfo.test_method = func_name\n\t\t\t\tdone_func = true\n\t\t\t\t# If the func line is left justified then there won't be any\n\t\t\t\t# inner classes above it.\n\t\t\t\tif(strip_text == text):\n\t\t\t\t\tdone_inner = true\n\n\t\t\tif(!done_inner and strip_text.begins_with(\"class\")):\n\t\t\t\tvar inner_name = _get_class_name_from_line(text)\n\t\t\t\tif(inner_name.begins_with(_inner_class_prefix)):\n\t\t\t\t\tinfo.inner_class = inner_name\n\t\t\t\t\tdone_inner = true\n\t\t\t\t\t# if we found an inner class then we are already past\n\t\t\t\t\t# any test the cursor could be in.\n\t\t\t\t\tdone_func = true\n\t\tline -= 1\n\n\treturn info\n\n\nfunc get_method_prefix():\n\treturn _method_prefix\n\n\nfunc set_method_prefix(method_prefix):\n\t_method_prefix = method_prefix\n\n\nfunc get_inner_class_prefix():\n\treturn _inner_class_prefix\n\n\nfunc set_inner_class_prefix(inner_class_prefix):\n\t_inner_class_prefix = inner_class_prefix\n"
  },
  {
    "path": "addons/gut/gui/script_text_editor_controls.gd.uid",
    "content": "uid://6xa0oyal4g4j\n"
  },
  {
    "path": "addons/gut/gut.gd",
    "content": "extends 'res://addons/gut/gut_to_move.gd'\nclass_name GutMain\n\n# ##############################################################################\n#\n# View the readme at https://github.com/bitwes/Gut/blob/master/README.md for usage\n# details.  You should also check out the github wiki at:\n# https://github.com/bitwes/Gut/wiki\n#\n# ##############################################################################\n\n\n# ###########################\n# Constants\n# ###########################\nconst LOG_LEVEL_FAIL_ONLY = 0\nconst LOG_LEVEL_TEST_AND_FAILURES = 1\nconst LOG_LEVEL_ALL_ASSERTS = 2\nconst WAITING_MESSAGE = '/# waiting #/'\nconst PAUSE_MESSAGE = '/# Pausing.  Press continue button...#/'\nconst COMPLETED = 'completed'\n\n# ###########################\n# Signals\n# ###########################\nsignal start_pause_before_teardown\nsignal end_pause_before_teardown\n\nsignal start_run\nsignal end_run\nsignal start_script(test_script_obj)\nsignal end_script\nsignal start_test(test_name)\nsignal end_test\n\n\n# ###########################\n# Settings\n#\n# These are properties that are usually set before a run is started through\n# gutconfig.\n# ###########################\n\nvar _inner_class_name = ''\n## When set, GUT will only run Inner-Test-Classes that contain this string.\nvar inner_class_name = _inner_class_name :\n\tget: return _inner_class_name\n\tset(val): _inner_class_name = val\n\nvar _ignore_pause_before_teardown = false\n## For batch processing purposes, you may want to ignore any calls to\n## pause_before_teardown that you forgot to remove_at.\nvar ignore_pause_before_teardown = _ignore_pause_before_teardown :\n\tget: return _ignore_pause_before_teardown\n\tset(val): _ignore_pause_before_teardown = val\n\nvar _log_level = 1\n## The log detail level.  Valid values are 0 - 2.  Larger values do not matter.\nvar log_level = _log_level:\n\tget: return _log_level\n\tset(val): _set_log_level(val)\n\n# TODO 4.0\n# This appears to not be used anymore.  Going to wait for more tests to be\n# ported before removing.\nvar _disable_strict_datatype_checks = false\nvar disable_strict_datatype_checks = false :\n\tget: return _disable_strict_datatype_checks\n\tset(val): _disable_strict_datatype_checks = val\n\nvar _export_path = ''\n## Path to file that GUT will create which holds a list of all test scripts so\n## that GUT can run tests when a project is exported.\nvar export_path = '' :\n\tget: return _export_path\n\tset(val): _export_path = val\n\nvar _include_subdirectories = false\n## Setting this to true will make GUT search all subdirectories of any directory\n## you have configured GUT to search for tests in.\nvar include_subdirectories = _include_subdirectories :\n\tget: return _include_subdirectories\n\tset(val): _include_subdirectories = val\n\n\nvar _double_strategy = GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY\n## TODO rework what this is and then document it here.\nvar double_strategy = _double_strategy  :\n\tget: return _double_strategy\n\tset(val):\n\t\tif(GutUtils.DOUBLE_STRATEGY.values().has(val)):\n\t\t\t_double_strategy = val\n\t\t\t_doubler.set_strategy(double_strategy)\n\t\telse:\n\t\t\t_lgr.error(str(\"gut.gd:  invalid double_strategy \", val))\n\nvar _pre_run_script = ''\n## Path to the script that will be run before all tests are run.  This script\n## must extend GutHookScript\nvar pre_run_script = _pre_run_script :\n\tget: return _pre_run_script\n\tset(val): _pre_run_script = val\n\nvar _post_run_script = ''\n## Path to the script that will run after all tests have run.  The script\n## must extend GutHookScript\nvar post_run_script = _post_run_script :\n\tget: return _post_run_script\n\tset(val): _post_run_script = val\n\nvar _color_output = false\n## Flag to color output at the command line and in the GUT GUI.\nvar color_output = false :\n\tget: return _color_output\n\tset(val):\n\t\t_color_output = val\n\t\t_lgr.disable_formatting(!_color_output)\n\nvar _junit_xml_file = ''\n## The full path to where GUT should write a JUnit compliant XML file to which\n## contains the results of all tests run.\nvar junit_xml_file = '' :\n\tget: return _junit_xml_file\n\tset(val): _junit_xml_file = val\n\nvar _junit_xml_timestamp = false\n## When true and junit_xml_file is set, the file name will include a\n## timestamp so that previous files are not overwritten.\nvar junit_xml_timestamp = false :\n\tget: return _junit_xml_timestamp\n\tset(val): _junit_xml_timestamp = val\n\n## The minimum amout of time GUT will wait before pausing for 1 frame to allow\n## the screen to paint.  GUT checkes after each test to see if enough time has\n## passed.\nvar paint_after = .1:\n\tget: return paint_after\n\tset(val): paint_after = val\n\nvar _unit_test_name = ''\n## When set GUT will only run tests that contain this string.\nvar unit_test_name = _unit_test_name :\n\tget: return _unit_test_name\n\tset(val): _unit_test_name = val\n\nvar _parameter_handler = null\n# This is populated by test.gd each time a paramterized test is encountered\n# for the first time.\n## FOR INTERNAL USE ONLY\nvar parameter_handler = _parameter_handler :\n\tget: return _parameter_handler\n\tset(val):\n\t\t_parameter_handler = val\n\t\t_parameter_handler.set_logger(_lgr)\n\nvar _lgr = GutUtils.get_logger()\n# Local reference for the common logger.\n## FOR INERNAL USE ONLY\nvar logger = _lgr :\n\tget: return _lgr\n\tset(val):\n\t\t_lgr = val\n\t\t_lgr.set_gut(self)\n\nvar _add_children_to = self\n# Sets the object that GUT will add test objects to as it creates them.  The\n# default is self, but can be set to other objects so that GUT is not obscured\n# by the objects added during tests.\n## FOR INERNAL USE ONLY\nvar add_children_to = self :\n\tget: return _add_children_to\n\tset(val): _add_children_to = val\n\n\nvar _treat_error_as_failure = true\nvar treat_error_as_failure = _treat_error_as_failure:\n\tget: return _treat_error_as_failure\n\tset(val): _treat_error_as_failure = val\n\n# ------------\n# Read only\n# ------------\nvar _test_collector = GutUtils.TestCollector.new()\nfunc get_test_collector():\n\treturn _test_collector\n\n# var version = null :\nfunc get_version():\n\treturn GutUtils.version_numbers.gut_version\n\nvar _orphan_counter =  GutUtils.OrphanCounter.new()\nfunc get_orphan_counter():\n\treturn _orphan_counter\n\nvar _autofree = GutUtils.AutoFree.new()\nfunc get_autofree():\n\treturn _autofree\n\nvar _stubber = GutUtils.Stubber.new()\nfunc get_stubber():\n\treturn _stubber\n\nvar _doubler = GutUtils.Doubler.new()\nfunc get_doubler():\n\treturn _doubler\n\nvar _spy = GutUtils.Spy.new()\nfunc get_spy():\n\treturn _spy\n\nvar _is_running = false\nfunc is_running():\n\treturn _is_running\n\n\n# ###########################\n# Private\n# ###########################\nvar  _should_print_versions = true # used to cut down on output in tests.\nvar _should_print_summary = true\n\nvar _file_prefix = 'test_'\nvar _inner_class_prefix = 'Test'\n\nvar _select_script = ''\nvar _last_paint_time = 0.0\nvar _strutils = GutUtils.Strutils.new()\n\n# The instance that is created from _pre_run_script.  Accessible from\n# get_pre_run_script_instance.  These are created at the start of the run\n# and then referenced at the appropriate time.  This allows us to validate the\n# scripts prior to running.\nvar _pre_run_script_instance = null\nvar _post_run_script_instance = null\n\nvar _script_name = null\n\n# The instanced scripts.  This is populated as the scripts are run.\nvar _test_script_objects = []\n\nvar _waiting = false\n\n# msecs ticks when run was started\nvar _start_time = 0.0\n\n# Collected Test instance for the current test being run.\nvar _current_test = null\nvar _pause_before_teardown = false\n\n\n# Used to cancel importing scripts if an error has occurred in the setup.  This\n# prevents tests from being run if they were exported and ensures that the\n# error displayed is seen since importing generates a lot of text.\n#\n# TODO this appears to only be checked and never set anywhere.  Verify that this\n# was not broken somewhere and remove if no longer used.\nvar _cancel_import = false\n\n# this is how long Gut will wait when there are items that must be queued free\n# when a test completes (due to calls to add_child_autoqfree)\nvar _auto_queue_free_delay = .1\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _init():\n\t# When running tests for GUT itself, GutUtils has been setup to always return\n\t# a new logger so this does not set the gut instance on the base logger\n\t# when creating test instances of GUT.\n\t_lgr.set_gut(self)\n\n\t_doubler.set_stubber(_stubber)\n\t_doubler.set_spy(_spy)\n\t_doubler.set_gut(self)\n\n\t# TODO remove_at these, universal logger should fix this.\n\t_doubler.set_logger(_lgr)\n\t_spy.set_logger(_lgr)\n\t_stubber.set_logger(_lgr)\n\t_test_collector.set_logger(_lgr)\n\n\n\n# ------------------------------------------------------------------------------\n# Initialize controls\n# ------------------------------------------------------------------------------\nfunc _ready():\n\tif(_should_print_versions):\n\t\t_lgr.log('---  GUT  ---')\n\t\t_lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.'))\n\n\tif(_select_script != null):\n\t\tselect_script(_select_script)\n\n\t_print_versions()\n\n# ------------------------------------------------------------------------------\n# Runs right before free is called.  Can't override `free`.\n# ------------------------------------------------------------------------------\nfunc _notification(what):\n\tif(what == NOTIFICATION_PREDELETE):\n\t\tfor ts in _test_script_objects:\n\t\t\tif(is_instance_valid(ts)):\n\t\t\t\tts.free()\n\n\t\t_test_script_objects = []\n\n\nfunc _print_versions(send_all = true):\n\tif(!_should_print_versions):\n\t\treturn\n\n\tvar info = GutUtils.version_numbers.get_version_text()\n\n\tif(send_all):\n\t\tp(info)\n\telse:\n\t\t_lgr.get_printer('gui').send(info + \"\\n\")\n\n\n\n\n# ####################\n#\n# Accessor code\n#\n# ####################\n\n\n# ------------------------------------------------------------------------------\n# Set the log level.  Use one of the various LOG_LEVEL_* constants.\n# ------------------------------------------------------------------------------\nfunc _set_log_level(level):\n\t_log_level = max(level, 0)\n\n\t# Level 0 settings\n\t_lgr.set_less_test_names(level == 0)\n\t# Explicitly always enabled\n\t_lgr.set_type_enabled(_lgr.types.normal, true)\n\t_lgr.set_type_enabled(_lgr.types.error, true)\n\t_lgr.set_type_enabled(_lgr.types.pending, true)\n\n\t# Level 1 types\n\t_lgr.set_type_enabled(_lgr.types.warn, level > 0)\n\t_lgr.set_type_enabled(_lgr.types.deprecated, level > 0)\n\n\t# Level 2 types\n\t_lgr.set_type_enabled(_lgr.types.passed, level > 1)\n\t_lgr.set_type_enabled(_lgr.types.info, level > 1)\n\t_lgr.set_type_enabled(_lgr.types.debug, level > 1)\n\n# ####################\n#\n# Events\n#\n# ####################\nfunc end_teardown_pause():\n\t_pause_before_teardown = false\n\t_waiting = false\n\tend_pause_before_teardown.emit()\n\n#####################\n#\n# Private\n#\n#####################\nfunc _log_test_children_warning(test_script):\n\tif(!_lgr.is_type_enabled(_lgr.types.orphan)):\n\t\treturn\n\n\tvar kids = test_script.get_children()\n\tif(kids.size() > 1):\n\t\tvar msg = ''\n\t\tif(_log_level == 2):\n\t\t\tmsg = \"Test script still has children when all tests finisehd.\\n\"\n\t\t\tfor i in range(kids.size()):\n\t\t\t\tmsg += str(\"  \", _strutils.type2str(kids[i]), \"\\n\")\n\t\t\tmsg += \"You can use autofree, autoqfree, add_child_autofree, or add_child_autoqfree to automatically free objects.\"\n\t\telse:\n\t\t\tmsg = str(\"Test script has \", kids.size(), \" unfreed children.  Increase log level for more details.\")\n\n\t\t_lgr.warn(msg)\n\n\nfunc _log_end_run():\n\tif(_should_print_summary):\n\t\tvar summary = GutUtils.Summary.new(self)\n\t\tsummary.log_end_run()\n\n\nfunc _validate_hook_script(path):\n\tvar result = {\n\t\tvalid = true,\n\t\tinstance = null\n\t}\n\n\t# empty path is valid but will have a null instance\n\tif(path == ''):\n\t\treturn result\n\n\tif(FileAccess.file_exists(path)):\n\t\tvar inst = load(path).new()\n\t\tif(inst and inst is GutHookScript):\n\t\t\tresult.instance = inst\n\t\t\tresult.valid = true\n\t\telse:\n\t\t\tresult.valid = false\n\t\t\t_lgr.error('The hook script [' + path + '] does not extend GutHookScript')\n\telse:\n\t\tresult.valid = false\n\t\t_lgr.error('The hook script [' + path + '] does not exist.')\n\n\treturn result\n\n\n# ------------------------------------------------------------------------------\n# Runs a hook script.  Script must exist, and must extend\n# GutHookScript or addons/gut/hook_script.gd\n# ------------------------------------------------------------------------------\nfunc _run_hook_script(inst):\n\tif(inst != null):\n\t\tinst.gut = self\n\t\tinst.run()\n\treturn inst\n\n# ------------------------------------------------------------------------------\n# Initialize variables for each run of a single test script.\n# ------------------------------------------------------------------------------\nfunc _init_run():\n\tvar valid = true\n\t_test_collector.set_test_class_prefix(_inner_class_prefix)\n\t_test_script_objects = []\n\t_current_test = null\n\t_is_running = true\n\n\tvar pre_hook_result = _validate_hook_script(_pre_run_script)\n\t_pre_run_script_instance = pre_hook_result.instance\n\tvar post_hook_result = _validate_hook_script(_post_run_script)\n\t_post_run_script_instance  = post_hook_result.instance\n\n\tvalid = pre_hook_result.valid and  post_hook_result.valid\n\n\treturn valid\n\n\n# ------------------------------------------------------------------------------\n# Print out run information and close out the run.\n# ------------------------------------------------------------------------------\nfunc _end_run():\n\t_log_end_run()\n\t_is_running = false\n\n\t_run_hook_script(get_post_run_script_instance())\n\t_export_results()\n\tend_run.emit()\n\n\n# ------------------------------------------------------------------------------\n# Add additional export types here.\n# ------------------------------------------------------------------------------\nfunc _export_results():\n\tif(_junit_xml_file != ''):\n\t\t_export_junit_xml()\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _export_junit_xml():\n\tvar exporter = GutUtils.JunitXmlExport.new()\n\tvar output_file = _junit_xml_file\n\n\tif(_junit_xml_timestamp):\n\t\tvar ext = \".\" + output_file.get_extension()\n\t\toutput_file = output_file.replace(ext, str(\"_\", Time.get_unix_time_from_system(), ext))\n\n\tvar f_result = exporter.write_file(self, output_file)\n\tif(f_result == OK):\n\t\tp(str(\"Results saved to \", output_file))\n\n\n# ------------------------------------------------------------------------------\n# Print out the heading for a new script\n# ------------------------------------------------------------------------------\nfunc _print_script_heading(coll_script):\n\tif(_does_class_name_match(_inner_class_name, coll_script.inner_class_name)):\n\t\t_lgr.log(str(\"\\n\\n\", coll_script.get_full_name()), _lgr.fmts.underline)\n\n\n# ------------------------------------------------------------------------------\n# Yes if the class name is null or the script's class name includes class_name\n# ------------------------------------------------------------------------------\nfunc _does_class_name_match(the_class_name, script_class_name):\n\treturn (the_class_name == null or the_class_name == '') or \\\n\t\t(script_class_name != null and str(script_class_name).findn(the_class_name) != -1)\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _setup_script(test_script, collected_script):\n\ttest_script.gut = self\n\ttest_script.set_logger(_lgr)\n\t_add_children_to.add_child(test_script)\n\t_test_script_objects.append(test_script)\n\n\tif(!test_script._was_ready_called):\n\t\ttest_script._do_ready_stuff()\n\t\t_lgr.warn(str(\"!!! YOU HAVE UPSET YOUR GUT !!!\\n\",\n\t\t\t\"You have overridden _ready in [\", collected_script.get_filename_and_inner(), \"] \",\n\t\t\t\"but it does not call super._ready().  New additions (or maybe old \",\n\t\t\t\"by the time you see this) require that super._ready() is called.\",\n\t\t\t\"\\n\\n\",\n\t\t\t\"GUT is working around this infraction, but may not be able to in \",\n\t\t\t\"the future.  GUT also reserves the right to decide it does not want \",\n\t\t\t\"to work around it in the future.  \",\n\t\t\t\"You should probably use before_all instead of _ready.  I can think \",\n\t\t\t\"of a few reasons why you would want to use _ready but I won't list \",\n\t\t\t\"them here because I think they are bad ideas.  I know they are bad \",\n\t\t\t\"ideas because I did them.  Hence the warning.  This message is \",\n\t\t\t\"intentially long so that it bothers you and you change your ways.\\n\\n\",\n\t\t\t\"Thank you for using GUT.\"))\n\n\n# ------------------------------------------------------------------------------\n# returns self so it can be integrated into the yield call.\n# ------------------------------------------------------------------------------\nfunc _wait_for_continue_button():\n\tp(PAUSE_MESSAGE, 0)\n\t_waiting = true\n\treturn self\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _get_indexes_matching_script_name(script_name):\n\tvar indexes = [] # empty runs all\n\tfor i in range(_test_collector.scripts.size()):\n\t\tif(_test_collector.scripts[i].get_filename().find(script_name) != -1):\n\t\t\tindexes.append(i)\n\treturn indexes\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _get_indexes_matching_path(path):\n\tvar indexes = []\n\tfor i in range(_test_collector.scripts.size()):\n\t\tif(_test_collector.scripts[i].path == path):\n\t\t\tindexes.append(i)\n\treturn indexes\n\n\n# ------------------------------------------------------------------------------\n# Execute all calls of a parameterized test.\n# ------------------------------------------------------------------------------\nfunc _run_parameterized_test(test_script, test_name):\n\tawait _run_test(test_script, test_name)\n\n\tif(_current_test.assert_count == 0 and !_current_test.pending):\n\t\t_lgr.risky('Test did not assert')\n\n\tif(_parameter_handler == null):\n\t\t_lgr.error(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.'))\n\t\t_fail(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.'))\n\telse:\n\t\twhile(!_parameter_handler.is_done()):\n\t\t\tvar cur_assert_count = _current_test.assert_count\n\t\t\tawait _run_test(test_script, test_name)\n\t\t\tif(_current_test.assert_count == cur_assert_count and !_current_test.pending):\n\t\t\t\t_lgr.risky('Test did not assert')\n\n\t_parameter_handler = null\n\n\n# ------------------------------------------------------------------------------\n# Runs a single test given a test.gd instance and the name of the test to run.\n# ------------------------------------------------------------------------------\nfunc _run_test(script_inst, test_name):\n\t_lgr.log_test_name()\n\t_lgr.set_indent_level(1)\n\t_orphan_counter.add_counter('test')\n\n\tawait script_inst.before_each()\n\n\tstart_test.emit(test_name)\n\n\tawait script_inst.call(test_name)\n\n\t# if the test called pause_before_teardown then await until\n\t# the continue button is pressed.\n\tif(_pause_before_teardown and !_ignore_pause_before_teardown):\n\t\tstart_pause_before_teardown.emit()\n\t\tawait _wait_for_continue_button().end_pause_before_teardown\n\n\tscript_inst.clear_signal_watcher()\n\n\t# call each post-each-test method until teardown is removed.\n\tawait script_inst.after_each()\n\n\t# Free up everything in the _autofree.  Yield for a bit if we\n\t# have anything with a queue_free so that they have time to\n\t# free and are not found by the orphan counter.\n\tvar aqf_count = _autofree.get_queue_free_count()\n\t_autofree.free_all()\n\tif(aqf_count > 0):\n\t\tawait get_tree().create_timer(_auto_queue_free_delay).timeout\n\n\tif(_log_level > 0):\n\t\t_orphan_counter.print_orphans('test', _lgr)\n\n\t_doubler.get_ignored_methods().clear()\n\n\n# ------------------------------------------------------------------------------\n# Calls after_all on the passed in test script and takes care of settings so all\n# logger output appears indented and with a proper heading\n#\n# Calls both pre-all-tests methods until prerun_setup is removed\n# ------------------------------------------------------------------------------\nfunc _call_before_all(test_script, collected_script):\n\tvar before_all_test_obj = GutUtils.CollectedTest.new()\n\tbefore_all_test_obj.has_printed_name = false\n\tbefore_all_test_obj.name = 'before_all'\n\n\tcollected_script.setup_teardown_tests.append(before_all_test_obj)\n\t_current_test = before_all_test_obj\n\n\t_lgr.inc_indent()\n\tawait test_script.before_all()\n\t# before all does not need to assert anything so only mark it as run if\n\t# some assert was done.\n\tbefore_all_test_obj.was_run = before_all_test_obj.did_something()\n\n\t_lgr.dec_indent()\n\n\t_current_test = null\n\n\n# ------------------------------------------------------------------------------\n# Calls after_all on the passed in test script and takes care of settings so all\n# logger output appears indented and with a proper heading\n#\n# Calls both post-all-tests methods until postrun_teardown is removed.\n# ------------------------------------------------------------------------------\nfunc _call_after_all(test_script, collected_script):\n\tvar after_all_test_obj = GutUtils.CollectedTest.new()\n\tafter_all_test_obj.has_printed_name = false\n\tafter_all_test_obj.name = 'after_all'\n\n\tcollected_script.setup_teardown_tests.append(after_all_test_obj)\n\t_current_test = after_all_test_obj\n\n\t_lgr.inc_indent()\n\tawait test_script.after_all()\n\t# after all does not need to assert anything so only mark it as run if\n\t# some assert was done.\n\tafter_all_test_obj.was_run = after_all_test_obj.did_something()\n\t_lgr.dec_indent()\n\n\t_current_test = null\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _should_skip_script(test_script, collected_script):\n\tvar skip_message = 'not skipped'\n\tvar skip_value = test_script.get('skip_script')\n\tvar should_skip = false\n\n\tif(skip_value == null):\n\t\tskip_value = test_script.should_skip_script()\n\telse:\n\t\t_lgr.deprecated('Using the skip_script var has been deprecated.  Implement the new should_skip_script() method in your test instead.')\n\n\tif(skip_value != null):\n\t\tif(typeof(skip_value) == TYPE_BOOL):\n\t\t\tshould_skip = skip_value\n\t\t\tif(skip_value):\n\t\t\t\tskip_message = 'script marked to skip'\n\t\telif(typeof(skip_value) == TYPE_STRING):\n\t\t\tshould_skip = true\n\t\t\tskip_message = skip_value\n\n\tif(should_skip):\n\t\tvar msg = str('- [Script skipped]:  ', skip_message)\n\t\t_lgr.inc_indent()\n\t\t_lgr.log(msg, _lgr.fmts.yellow)\n\t\t_lgr.dec_indent()\n\t\tcollected_script.skip_reason = skip_message\n\t\tcollected_script.was_skipped = true\n\n\treturn should_skip\n\n# ------------------------------------------------------------------------------\n# Run all tests in a script.  This is the core logic for running tests.\n# ------------------------------------------------------------------------------\nfunc _test_the_scripts(indexes=[]):\n\t_orphan_counter.add_counter('pre_run')\n\n\t_print_versions(false)\n\tvar is_valid = _init_run()\n\tif(!is_valid):\n\t\t_lgr.error('Something went wrong and the run was aborted.')\n\t\treturn\n\n\t_run_hook_script(get_pre_run_script_instance())\n\tif(_pre_run_script_instance!= null and _pre_run_script_instance.should_abort()):\n\t\t_lgr.error('pre-run abort')\n\t\tend_run.emit()\n\t\treturn\n\n\tstart_run.emit()\n\t_start_time = Time.get_ticks_msec()\n\t_last_paint_time = _start_time\n\n\tvar indexes_to_run = []\n\tif(indexes.size()==0):\n\t\tfor i in range(_test_collector.scripts.size()):\n\t\t\tindexes_to_run.append(i)\n\telse:\n\t\tindexes_to_run = indexes\n\n\n\t# loop through scripts\n\tfor test_indexes in range(indexes_to_run.size()):\n\t\tvar coll_script = _test_collector.scripts[indexes_to_run[test_indexes]]\n\t\t_orphan_counter.add_counter('script')\n\n\t\tif(coll_script.tests.size() > 0):\n\t\t\t_lgr.set_indent_level(0)\n\t\t\t_print_script_heading(coll_script)\n\n\t\tif(!coll_script.is_loaded):\n\t\t\tbreak\n\n\t\tstart_script.emit(coll_script)\n\n\t\tvar test_script = coll_script.get_new()\n\n\t\t_setup_script(test_script, coll_script)\n\t\t_doubler.set_strategy(_double_strategy)\n\n\t\t# ----\n\t\t# SHORTCIRCUIT\n\t\t# skip_script logic\n\t\tif(_should_skip_script(test_script, coll_script)):\n\t\t\tcontinue\n\t\t# ----\n\n\t\t# !!!\n\t\t# Hack so there isn't another indent to this monster of a method.  if\n\t\t# inner class is set and we do not have a match then empty the tests\n\t\t# for the current test.\n\t\t# !!!\n\t\tif(!_does_class_name_match(_inner_class_name, coll_script.inner_class_name)):\n\t\t\tcoll_script.tests = []\n\t\telse:\n\t\t\tcoll_script.was_run = true\n\t\t\tawait _call_before_all(test_script, coll_script)\n\n\t\t# Each test in the script\n\t\tfor i in range(coll_script.tests.size()):\n\t\t\t_stubber.clear()\n\t\t\t_spy.clear()\n\t\t\t_current_test = coll_script.tests[i]\n\n\t\t\tif((_unit_test_name != '' and _current_test.name.find(_unit_test_name) > -1) or\n\t\t\t\t(_unit_test_name == '')):\n\n\t\t\t\tvar ticks_before := Time.get_ticks_usec()\n\n\t\t\t\tif(_current_test.arg_count > 1):\n\t\t\t\t\t_lgr.error(str('Parameterized test ', _current_test.name,\n\t\t\t\t\t\t' has too many parameters:  ', _current_test.arg_count, '.'))\n\t\t\t\telif(_current_test.arg_count == 1):\n\t\t\t\t\t_current_test.was_run = true\n\t\t\t\t\tawait _run_parameterized_test(test_script, _current_test.name)\n\t\t\t\telse:\n\t\t\t\t\t_current_test.was_run = true\n\t\t\t\t\tawait _run_test(test_script, _current_test.name)\n\n\t\t\t\tif(!_current_test.did_something()):\n\t\t\t\t\t_lgr.risky(str(_current_test.name, ' did not assert'))\n\n\t\t\t\t_current_test.has_printed_name = false\n\n\t\t\t\t_current_test.time_taken = (Time.get_ticks_usec() - ticks_before) / 1000000.0\n\n\t\t\t\tend_test.emit()\n\n\t\t\t\t# After each test, check to see if we shoudl wait a frame to\n\t\t\t\t# paint based on how much time has elapsed since we last 'painted'\n\t\t\t\tif(paint_after > 0.0):\n\t\t\t\t\tvar now = Time.get_ticks_msec()\n\t\t\t\t\tvar time_since = (now - _last_paint_time) / 1000.0\n\t\t\t\t\tif(time_since > paint_after):\n\t\t\t\t\t\t_last_paint_time = now\n\t\t\t\t\t\tawait get_tree().process_frame\n\n\t\t_current_test = null\n\t\t_lgr.dec_indent()\n\t\t_orphan_counter.print_orphans('script', _lgr)\n\n\t\tif(_does_class_name_match(_inner_class_name, coll_script.inner_class_name)):\n\t\t\tawait _call_after_all(test_script, coll_script)\n\n\t\t_log_test_children_warning(test_script)\n\t\t# This might end up being very resource intensive if the scripts\n\t\t# don't clean up after themselves.  Might have to consolidate output\n\t\t# into some other structure and kill the script objects with\n\t\t# test_script.free() instead of remove_at child.\n\t\t_add_children_to.remove_child(test_script)\n\n\t\t_lgr.set_indent_level(0)\n\t\tif(test_script.get_assert_count() > 0):\n\t\t\tvar script_sum = str(coll_script.get_passing_test_count(), '/', coll_script.get_ran_test_count(), ' passed.')\n\t\t\t_lgr.log(script_sum, _lgr.fmts.bold)\n\n\t\tend_script.emit()\n\t\t# END TEST SCRIPT LOOP\n\n\t_lgr.set_indent_level(0)\n\t# Give anything that is queued to be freed time to be freed before we count\n\t# the orphans.  Without this, the last test's awaiter won't be freed\n\t# yet, which messes with the orphans total.  There could also be objects\n\t# the user has queued to be freed as well.\n\tawait get_tree().create_timer(.1).timeout\n\t_end_run()\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _pass(text=''):\n\tif(_current_test):\n\t\t_current_test.add_pass(text)\n\n\n# ------------------------------------------------------------------------------\n# Returns an empty string or \"(call #x) \" if the current test being run has\n# parameters.  The\n# ------------------------------------------------------------------------------\nfunc get_call_count_text():\n\tvar to_return = ''\n\tif(_parameter_handler != null):\n\t\t# This uses get_call_count -1 because test.gd's use_parameters method\n\t\t# should have been called before we get to any calls for this method\n\t\t# just due to how use_parameters works.  There isn't a way to know\n\t\t# whether we are before or after that call.\n\t\tto_return = str('params[', _parameter_handler.get_call_count() -1, '] ')\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _fail(text=''):\n\tif(_current_test != null):\n\t\tvar line_number = _extract_line_number(_current_test)\n\t\tvar line_text = '  at line ' + str(line_number)\n\t\tp(line_text, LOG_LEVEL_FAIL_ONLY)\n\t\t# format for summary\n\t\tline_text =  \"\\n    \" + line_text\n\t\tvar call_count_text = get_call_count_text()\n\t\t_current_test.line_number = line_number\n\t\t_current_test.add_fail(call_count_text + text + line_text)\n\n\n# ------------------------------------------------------------------------------\n# This is \"private\" but is only used by the logger, it is not used internally.\n# It was either, make this weird method or \"do it the right way\" with signals\n# or some other crazy mechanism.\n# ------------------------------------------------------------------------------\nfunc _fail_for_error(err_text):\n\tif(_current_test != null and treat_error_as_failure):\n\t\t_fail(err_text)\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _pending(text=''):\n\tif(_current_test):\n\t\t_current_test.add_pending(text)\n\n\n# ------------------------------------------------------------------------------\n# Extracts the line number from curren stacktrace by matching the test case name\n# ------------------------------------------------------------------------------\nfunc _extract_line_number(current_test):\n\tvar line_number = -1\n\t# if stack trace available than extraxt the test case line number\n\tvar stackTrace = get_stack()\n\tif(stackTrace!=null):\n\t\tfor index in stackTrace.size():\n\t\t\tvar line = stackTrace[index]\n\t\t\tvar function = line.get(\"function\")\n\t\t\tif function == current_test.name:\n\t\t\t\tline_number = line.get(\"line\")\n\treturn line_number\n\n\n# ------------------------------------------------------------------------------\n# Gets all the files in a directory and all subdirectories if include_subdirectories\n# is true.  The files returned are all sorted by name.\n# ------------------------------------------------------------------------------\nfunc _get_files(path, prefix, suffix):\n\tvar files = []\n\tvar directories = []\n\t# ignore addons/gut per issue 294\n\tif(path == 'res://addons/gut'):\n\t\treturn [];\n\n\tvar d = DirAccess.open(path)\n\td.include_hidden = false\n\td.include_navigational = false\n\n\t# Traversing a directory is kinda odd.  You have to start the process of\n\t# listing the contents of a directory with list_dir_begin then use get_next\n\t# until it returns an empty string.  Then I guess you should end it.\n\td.list_dir_begin()\n\tvar fs_item = d.get_next()\n\tvar full_path = ''\n\twhile(fs_item != ''):\n\t\tfull_path = path.path_join(fs_item)\n\n\t\t# MUST use FileAccess since d.file_exists returns false for exported\n\t\t# projects\n\t\tif(FileAccess.file_exists(full_path)):\n\t\t\tif(fs_item.begins_with(prefix) and fs_item.ends_with(suffix)):\n\t\t\t\tfiles.append(full_path)\n\t\t# MUST use DirAccess, d.dir_exists is false for exported projects.\n\t\telif(include_subdirectories and DirAccess.dir_exists_absolute(full_path)):\n\t\t\tdirectories.append(full_path)\n\n\t\tfs_item = d.get_next()\n\td.list_dir_end()\n\n\tfor dir in range(directories.size()):\n\t\tvar dir_files = _get_files(directories[dir], prefix, suffix)\n\t\tfor i in range(dir_files.size()):\n\t\t\tfiles.append(dir_files[i])\n\n\tfiles.sort()\n\treturn files\n\n\n#########################\n#\n# public\n#\n#########################\nfunc get_elapsed_time() -> float:\n\tvar to_return = 0.0\n\tif(_start_time != 0.0):\n\t\tto_return = Time.get_ticks_msec() - _start_time\n\tto_return = to_return / 1000.0\n\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# Conditionally prints the text to the console/results variable based on the\n# current log level and what level is passed in.  Whenever currently in a test,\n# the text will be indented under the test.  It can be further indented if\n# desired.\n#\n# The first time output is generated when in a test, the test name will be\n# printed.\n# ------------------------------------------------------------------------------\nfunc p(text, level=0):\n\tvar str_text = str(text)\n\n\tif(level <= GutUtils.nvl(_log_level, 0)):\n\t\t_lgr.log(str_text)\n\n################\n#\n# RUN TESTS/ADD SCRIPTS\n#\n################\n\n# ------------------------------------------------------------------------------\n# Runs all the scripts that were added using add_script\n# ------------------------------------------------------------------------------\nfunc test_scripts(_run_rest=false):\n\tif(_script_name != null and _script_name != ''):\n\t\tvar indexes = _get_indexes_matching_script_name(_script_name)\n\t\tif(indexes == []):\n\t\t\t_lgr.error(str(\n\t\t\t\t\"Could not find script matching '\", _script_name, \"'.\\n\",\n\t\t\t\t\"Check your directory settings and Script Prefix/Suffix settings.\"))\n\t\t\tend_run.emit()\n\t\telse:\n\t\t\t_test_the_scripts(indexes)\n\telse:\n\t\t_test_the_scripts([])\n\n# alias\nfunc run_tests(run_rest=false):\n\ttest_scripts(run_rest)\n\n\n# ------------------------------------------------------------------------------\n# Runs a single script passed in.\n# ------------------------------------------------------------------------------\n# func run_test_script(script):\n# \t_test_collector.set_test_class_prefix(_inner_class_prefix)\n# \t_test_collector.clear()\n# \t_test_collector.add_script(script)\n# \t_test_the_scripts()\n\n\n# ------------------------------------------------------------------------------\n# Adds a script to be run when test_scripts called.\n# ------------------------------------------------------------------------------\nfunc add_script(script):\n\tif(!Engine.is_editor_hint()):\n\t\t_test_collector.set_test_class_prefix(_inner_class_prefix)\n\t\t_test_collector.add_script(script)\n\n\n# ------------------------------------------------------------------------------\n# Add all scripts in the specified directory that start with the prefix and end\n# with the suffix.  Does not look in sub directories.  Can be called multiple\n# times.\n# ------------------------------------------------------------------------------\nfunc add_directory(path, prefix=_file_prefix, suffix=\".gd\"):\n\t# check for '' b/c the calls to addin the exported directories 1-6 will pass\n\t# '' if the field has not been populated.  This will cause res:// to be\n\t# processed which will include all files if include_subdirectories is true.\n\tif(path == '' or path == null):\n\t\treturn\n\n\tvar dir = DirAccess.open(path)\n\tif(dir == null):\n\t\t_lgr.error(str('The path [', path, '] does not exist.'))\n\telse:\n\t\tvar files = _get_files(path, prefix, suffix)\n\t\tfor i in range(files.size()):\n\t\t\tif(_script_name == null or _script_name == '' or \\\n\t\t\t\t\t(_script_name != null and files[i].findn(_script_name) != -1)):\n\t\t\t\tadd_script(files[i])\n\n\n# ------------------------------------------------------------------------------\n# This will try to find a script in the list of scripts to test that contains\n# the specified script name.  It does not have to be a full match.  It will\n# select the first matching occurrence so that this script will run when run_tests\n# is called.  Works the same as the select_this_one option of add_script.\n#\n# returns whether it found a match or not\n# ------------------------------------------------------------------------------\nfunc select_script(script_name):\n\t_script_name = script_name\n\t_select_script = script_name\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc export_tests(path=_export_path):\n\tif(path == null):\n\t\t_lgr.error('You must pass a path or set the export_path before calling export_tests')\n\telse:\n\t\tvar result = _test_collector.export_tests(path)\n\t\tif(result):\n\t\t\t_lgr.info(_test_collector.to_s())\n\t\t\t_lgr.info(\"Exported to \" + path)\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc import_tests(path=_export_path):\n\tif(!FileAccess.file_exists(path)):\n\t\t_lgr.error(str('Cannot import tests:  the path [', path, '] does not exist.'))\n\telse:\n\t\t_test_collector.clear()\n\t\tvar result = _test_collector.import_tests(path)\n\t\tif(result):\n\t\t\t_lgr.info(\"\\n\" + _test_collector.to_s())\n\t\t\t_lgr.info(\"Imported from \" + path)\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc import_tests_if_none_found():\n\tif(!_cancel_import and _test_collector.scripts.size() == 0):\n\t\timport_tests()\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc export_if_tests_found():\n\tif(_test_collector.scripts.size() > 0):\n\t\texport_tests()\n\n################\n#\n# MISC\n#\n################\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc maximize():\n\t_lgr.deprecated('gut.maximize')\n\n\n# ------------------------------------------------------------------------------\n# Clears the text of the text box.  This resets all counters.\n# ------------------------------------------------------------------------------\nfunc clear_text():\n\t_lgr.deprecated('gut.clear_text')\n\n\n# ------------------------------------------------------------------------------\n# Get the number of tests that were ran\n# ------------------------------------------------------------------------------\nfunc get_test_count():\n\treturn _test_collector.get_ran_test_count()\n\n# ------------------------------------------------------------------------------\n# Get the number of assertions that were made\n# ------------------------------------------------------------------------------\nfunc get_assert_count():\n\treturn _test_collector.get_assert_count()\n\n# ------------------------------------------------------------------------------\n# Get the number of assertions that passed\n# ------------------------------------------------------------------------------\nfunc get_pass_count():\n\treturn _test_collector.get_pass_count()\n\n# ------------------------------------------------------------------------------\n# Get the number of assertions that failed\n# ------------------------------------------------------------------------------\nfunc get_fail_count():\n\treturn _test_collector.get_fail_count()\n\n# ------------------------------------------------------------------------------\n# Get the number of tests flagged as pending\n# ------------------------------------------------------------------------------\nfunc get_pending_count():\n\treturn _test_collector.get_pending_count()\n\n\n# ------------------------------------------------------------------------------\n# Call this method to make the test pause before teardown so that you can inspect\n# anything that you have rendered to the screen.\n# ------------------------------------------------------------------------------\nfunc pause_before_teardown():\n\t_pause_before_teardown = true;\n\n\n# ------------------------------------------------------------------------------\n# Returns the script object instance that is currently being run.\n# ------------------------------------------------------------------------------\nfunc get_current_script_object():\n\tvar to_return = null\n\tif(_test_script_objects.size() > 0):\n\t\tto_return = _test_script_objects[-1]\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_current_test_object():\n\treturn _current_test\n\n\n## Returns a summary.gd object that contains all the information about\n## the run results.\nfunc get_summary():\n\treturn GutUtils.Summary.new(self)\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_pre_run_script_instance():\n\treturn _pre_run_script_instance\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_post_run_script_instance():\n\treturn _post_run_script_instance\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc show_orphans(should):\n\t_lgr.set_type_enabled(_lgr.types.orphan, should)\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_logger():\n\treturn _lgr\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_test_script_count():\n\treturn _test_script_objects.size()\n\n\n\n\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gut.gd.uid",
    "content": "uid://dr1jwb3s6t8cc\n"
  },
  {
    "path": "addons/gut/gut_cmdln.gd",
    "content": "# ------------------------------------------------------------------------------\n# Description\n# -----------\n# Command line interface for the GUT unit testing tool.  Allows you to run tests\n# from the command line instead of running a scene.  You can run this script\n# (from the root of your project) using the following command:\n# \tgodot -s test/gut/gut_cmdln.gd\n#\n# See the wiki for a list of options and examples.  You can also use the -gh\n# option to get more information about how to use the command line interface.\n# ------------------------------------------------------------------------------\nextends SceneTree\n\nfunc _init():\n\tvar max_iter = 20\n\tvar iter = 0\n\n\t# Not seen this wait more than 1.\n\twhile(Engine.get_main_loop() == null and iter < max_iter):\n\t\tawait create_timer(.01).timeout\n\t\titer += 1\n\n\tif(Engine.get_main_loop() == null):\n\t\tpush_error('Main loop did not start in time.')\n\t\tquit(0)\n\t\treturn\n\n\tvar cli = load('res://addons/gut/cli/gut_cli.gd').new()\n\tget_root().add_child(cli)\n\tcli.main()\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gut_cmdln.gd.uid",
    "content": "uid://v7te5ep68nve\n"
  },
  {
    "path": "addons/gut/gut_config.gd",
    "content": "# ##############################################################################\n#\n# This holds all the configuratoin values for GUT.  It can load and save values\n# to a json file.  It is also responsible for applying these settings to GUT.\n#\n# ##############################################################################\nvar valid_fonts = ['AnonymousPro', 'CourierPro', 'LobsterTwo', 'Default']\n\nvar default_options = {\n\tbackground_color = Color(.15, .15, .15, 1).to_html(),\n\tconfig_file = 'res://.gutconfig.json',\n\t# used by editor to handle enabled/disabled dirs.  All dirs configured go\n\t# here and only the enabled dirs go into dirs\n\tconfigured_dirs = [],\n\tdirs = [],\n\tdisable_colors = false,\n\t# double strategy can be the name of the enum value, the enum value or\n\t# lowercase name with spaces:  0/SCRIPT_ONLY/script only\n\t# The GUI gut config expects the value to be the enum value and not a string\n\t# when saved.\n\tdouble_strategy = 'SCRIPT_ONLY',\n\t# named differently than gut option so we can use it as a flag in the cli\n\terrors_do_not_cause_failure = false,\n\tfont_color = Color(.8, .8, .8, 1).to_html(),\n\tfont_name = 'CourierPrime',\n\tfont_size = 16,\n\thide_orphans = false,\n\tignore_pause = false,\n\tinclude_subdirs = false,\n\tinner_class = '',\n\tjunit_xml_file = '',\n\tjunit_xml_timestamp = false,\n\tlog_level = 1,\n\topacity = 100,\n\tpaint_after = .1,\n\tpost_run_script = '',\n\tpre_run_script = '',\n\tprefix = 'test_',\n\tselected = '',\n\tshould_exit = false,\n\tshould_exit_on_success = false,\n\tshould_maximize = false,\n\tcompact_mode = false,\n\tshow_help = false,\n\tsuffix = '.gd',\n\ttests = [],\n\tunit_test_name = '',\n\n\tgut_on_top = true,\n}\n\n\nvar options = default_options.duplicate()\n\n\nfunc _null_copy(h):\n\tvar new_hash = {}\n\tfor key in h:\n\t\tnew_hash[key] = null\n\treturn new_hash\n\n\nfunc _load_options_from_config_file(file_path, into):\n\t# SHORTCIRCUIT\n\tif(!FileAccess.file_exists(file_path)):\n\t\tif(file_path != 'res://.gutconfig.json'):\n\t\t\tprint('ERROR:  Config File \"', file_path, '\" does not exist.')\n\t\t\treturn -1\n\t\telse:\n\t\t\treturn 1\n\n\tvar f = FileAccess.open(file_path, FileAccess.READ)\n\tif(f == null):\n\t\tvar result = FileAccess.get_open_error()\n\t\tpush_error(str(\"Could not load data \", file_path, ' ', result))\n\t\treturn result\n\n\tvar json = f.get_as_text()\n\tf = null # close file\n\n\tvar test_json_conv = JSON.new()\n\ttest_json_conv.parse(json)\n\tvar results = test_json_conv.get_data()\n\t# SHORTCIRCUIT\n\tif(results == null):\n\t\tprint(\"\\n\\n\",'!! ERROR parsing file:  ', file_path)\n\t\treturn -1\n\n\t# Get all the options out of the config file using the option name.  The\n\t# options hash is now the default source of truth for the name of an option.\n\t_load_dict_into(results, into)\n\n\treturn 1\n\nfunc _load_dict_into(source, dest):\n\tfor key in dest:\n\t\tif(source.has(key)):\n\t\t\tif(source[key] != null):\n\t\t\t\tif(typeof(source[key]) == TYPE_DICTIONARY):\n\t\t\t\t\t_load_dict_into(source[key], dest[key])\n\t\t\t\telse:\n\t\t\t\t\tdest[key] = source[key]\n\n\n# Apply all the options specified to tester.  This is where the rubber meets\n# the road.\nfunc _apply_options(opts, gut):\n\tgut.include_subdirectories = opts.include_subdirs\n\n\tif(opts.inner_class != ''):\n\t\tgut.inner_class_name = opts.inner_class\n\tgut.log_level = opts.log_level\n\tgut.ignore_pause_before_teardown = opts.ignore_pause\n\n\tgut.select_script(opts.selected)\n\n\tfor i in range(opts.dirs.size()):\n\t\tgut.add_directory(opts.dirs[i], opts.prefix, opts.suffix)\n\n\tfor i in range(opts.tests.size()):\n\t\tgut.add_script(opts.tests[i])\n\n\t# Sometimes it is the index, sometimes it's a string.  This sets it regardless\n\tgut.double_strategy = GutUtils.get_enum_value(\n\t\topts.double_strategy, GutUtils.DOUBLE_STRATEGY,\n\t\tGutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY)\n\n\tgut.unit_test_name = opts.unit_test_name\n\tgut.pre_run_script = opts.pre_run_script\n\tgut.post_run_script = opts.post_run_script\n\tgut.color_output = !opts.disable_colors\n\tgut.show_orphans(!opts.hide_orphans)\n\tgut.junit_xml_file = opts.junit_xml_file\n\tgut.junit_xml_timestamp = opts.junit_xml_timestamp\n\tgut.paint_after = str(opts.paint_after).to_float()\n\tgut.treat_error_as_failure = !opts.errors_do_not_cause_failure\n\n\treturn gut\n\n# --------------------------\n# Public\n# --------------------------\nfunc write_options(path):\n\tvar content = JSON.stringify(options, ' ')\n\n\tvar f = FileAccess.open(path, FileAccess.WRITE)\n\tvar result = FileAccess.get_open_error()\n\tif(f != null):\n\t\tf.store_string(content)\n\t\tf = null # closes file\n\telse:\n\t\tprint('ERROR:  could not open file ', path, ' ', result)\n\treturn result\n\n\n# consistent name\nfunc save_file(path):\n\twrite_options(path)\n\n\nfunc load_options(path):\n\treturn _load_options_from_config_file(path, options)\n\n\n# consistent name\nfunc load_file(path):\n\treturn load_options(path)\n\n\nfunc load_options_no_defaults(path):\n\toptions = _null_copy(default_options)\n\treturn _load_options_from_config_file(path, options)\n\n\nfunc apply_options(gut):\n\t_apply_options(options, gut)\n\n\n\n\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gut_config.gd.uid",
    "content": "uid://d3ne8p2wm5y8d\n"
  },
  {
    "path": "addons/gut/gut_plugin.gd",
    "content": "@tool\nextends EditorPlugin\n\nvar _bottom_panel = null\n\nfunc _version_conversion():\n\tvar EditorGlobals = load(\"res://addons/gut/gui/editor_globals.gd\")\n\tEditorGlobals.create_temp_directory()\n\n\tvar VersionConversion = load(\"res://addons/gut/version_conversion.gd\")\n\tVersionConversion.convert()\n\nfunc _enter_tree():\n\t_version_conversion()\n\n\t_bottom_panel = preload('res://addons/gut/gui/GutBottomPanel.tscn').instantiate()\n\n\tvar button = add_control_to_bottom_panel(_bottom_panel, 'GUT')\n\tbutton.shortcut_in_tooltip = true\n\n\t# ---------\n\t# I removed this delay because it was causing issues with the shortcut button.\n\t# The shortcut button wouldn't work right until load_shortcuts is called., but\n\t# the delay gave you 3 seconds to click it before they were loaded.  This\n\t# await came with the conversion to 4 and probably isn't needed anymore.\n\t# I'm leaving it here becuase I don't know why it showed up to begin with\n\t# and if it's needed, it will be pretty hard to debug without seeing this.\n\t#\n\t# This should be deleted after the next release or two if not needed.\n\t# await get_tree().create_timer(3).timeout\n\t# ---\n\t_bottom_panel.set_interface(get_editor_interface())\n\t_bottom_panel.set_plugin(self)\n\t_bottom_panel.set_panel_button(button)\n\t_bottom_panel.load_shortcuts()\n\n\nfunc _exit_tree():\n\t# Clean-up of the plugin goes here\n\t# Always remember to remove_at it from the engine when deactivated\n\tremove_control_from_bottom_panel(_bottom_panel)\n\t_bottom_panel.free()\n\n\n# This seems like a good idea at first, but it deletes the settings for ALL\n# projects.  If by chance you want to do that you can uncomment this, reload the\n# project and then disable GUT.\n# func _disable_plugin():\n#\tvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\n# \tGutEditorGlobals.user_prefs.erase_all()"
  },
  {
    "path": "addons/gut/gut_plugin.gd.uid",
    "content": "uid://2u2kcxykntbf\n"
  },
  {
    "path": "addons/gut/gut_to_move.gd",
    "content": "# Temporary base script for gut.gd to hold the things to be remvoed and added\n# to some utility somewhere.\nextends Node\n\n# ------------------------------------------------------------------------------\n# deletes all files in a given directory\n# ------------------------------------------------------------------------------\nfunc directory_delete_files(path):\n\tvar d = DirAccess.open(path)\n\n\t# SHORTCIRCUIT\n\tif(d == null):\n\t\treturn\n\n\t# Traversing a directory is kinda odd.  You have to start the process of listing\n\t# the contents of a directory with list_dir_begin then use get_next until it\n\t# returns an empty string.  Then I guess you should end it.\n\td.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547\n\tvar thing = d.get_next() # could be a dir or a file or something else maybe?\n\tvar full_path = ''\n\twhile(thing != ''):\n\t\tfull_path = path + \"/\" + thing\n\t\t# file_exists returns fasle for directories\n\t\tif(d.file_exists(full_path)):\n\t\t\td.remove(full_path)\n\t\tthing = d.get_next()\n\n\td.list_dir_end()\n\n# ------------------------------------------------------------------------------\n# deletes the file at the specified path\n# ------------------------------------------------------------------------------\nfunc file_delete(path):\n\tvar d = DirAccess.open(path.get_base_dir())\n\tif(d != null):\n\t\td.remove(path)\n\n# ------------------------------------------------------------------------------\n# Checks to see if the passed in file has any data in it.\n# ------------------------------------------------------------------------------\nfunc is_file_empty(path):\n\tvar f = FileAccess.open(path, FileAccess.READ)\n\tvar result = FileAccess.get_open_error()\n\tvar empty = true\n\tif(result == OK):\n\t\tempty = f.get_length() == 0\n\tf = null\n\treturn empty\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc get_file_as_text(path):\n\treturn GutUtils.get_file_as_text(path)\n\n# ------------------------------------------------------------------------------\n# Creates an empty file at the specified path\n# ------------------------------------------------------------------------------\nfunc file_touch(path):\n\tFileAccess.open(path, FileAccess.WRITE)\n\n# ------------------------------------------------------------------------------\n# Simulate a number of frames by calling '_process' and '_physics_process' (if\n# the methods exist) on an object and all of its descendents. The specified frame\n# time, 'delta', will be passed to each simulated call.\n#\n# NOTE: Objects can disable their processing methods using 'set_process(false)' and\n# 'set_physics_process(false)'. This is reflected in the 'Object' methods\n# 'is_processing()' and 'is_physics_processing()', respectively. To make 'simulate'\n# respect this status, for example if you are testing an object which toggles\n# processing, pass 'check_is_processing' as 'true'.\n# ------------------------------------------------------------------------------\nfunc simulate(obj, times, delta, check_is_processing: bool = false):\n\tfor _i in range(times):\n\t\tif (\n\t\t\tobj.has_method(\"_process\")\n\t\t\tand (\n\t\t\t\tnot check_is_processing\n\t\t\t\tor obj.is_processing()\n\t\t\t)\n\t\t):\n\t\t\tobj._process(delta)\n\t\tif(\n\t\t\tobj.has_method(\"_physics_process\")\n\t\t\tand (\n\t\t\t\tnot check_is_processing\n\t\t\t\tor obj.is_physics_processing()\n\t\t\t)\n\t\t):\n\t\t\tobj._physics_process(delta)\n\n\t\tfor kid in obj.get_children():\n\t\t\tsimulate(kid, 1, delta, check_is_processing)\n"
  },
  {
    "path": "addons/gut/gut_to_move.gd.uid",
    "content": "uid://rvkrakssev4v\n"
  },
  {
    "path": "addons/gut/gut_vscode_debugger.gd",
    "content": "# ------------------------------------------------------------------------------\n# Entry point for using the debugger through VSCode.  The gut-extension for\n# VSCode launches this instead of gut_cmdln.gd when running tests through the\n# debugger.\n#\n# This could become more complex overtime, but right now all we have to do is\n# to make sure the console printer is enabled or you do not get any output.\n# ------------------------------------------------------------------------------\nextends 'res://addons/gut/gut_cmdln.gd'\n\nfunc run_tests(runner):\n\trunner.get_gut().get_logger().disable_printer('console', false)\n\trunner.run_tests()\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n"
  },
  {
    "path": "addons/gut/gut_vscode_debugger.gd.uid",
    "content": "uid://d3nym0a7c2s22\n"
  },
  {
    "path": "addons/gut/hook_script.gd",
    "content": "class_name GutHookScript\n# ------------------------------------------------------------------------------\n# This script is the base for custom scripts to be used in pre and post\n# run hooks.\n#\n# To use, inherit from this script and then implement the run method.\n# ------------------------------------------------------------------------------\nvar JunitXmlExport = load('res://addons/gut/junit_xml_export.gd')\n\n# This is the instance of GUT that is running the tests.  You can get\n# information about the run from this object.  This is set by GUT when the\n# script is instantiated.\nvar gut  = null\n\n# the exit code to be used by gut_cmdln.  See set method.\nvar _exit_code = null\n\nvar _should_abort =  false\n\n# Virtual method that will be called by GUT after instantiating\n# this script.\nfunc run():\n\tgut.logger.error(\"Run method not overloaded.  Create a 'run()' method in your hook script to run your code.\")\n\n\n# Set the exit code when running from the command line.  If not set then the\n# default exit code will be returned (0 when no tests fail, 1 when any tests\n# fail).\nfunc set_exit_code(code):\n\t_exit_code  = code\n\nfunc get_exit_code():\n\treturn _exit_code\n\n# Usable by pre-run script to cause the run to end AFTER the run() method\n# finishes.  post-run script will not be ran.\nfunc abort():\n\t_should_abort = true\n\nfunc should_abort():\n\treturn _should_abort\n"
  },
  {
    "path": "addons/gut/hook_script.gd.uid",
    "content": "uid://1csg1kalv3eq\n"
  },
  {
    "path": "addons/gut/icon.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bvo0uao7deu0q\"\npath=\"res://.godot/imported/icon.png-91b084043b8aaf2f1c906e7b9fa92969.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/icon.png\"\ndest_files=[\"res://.godot/imported/icon.png-91b084043b8aaf2f1c906e7b9fa92969.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/images/Folder.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dhvl14hls3y2j\"\npath=\"res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/images/Folder.svg\"\ndest_files=[\"res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "addons/gut/images/Script.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cavojn74qp7ij\"\npath=\"res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/images/Script.svg\"\ndest_files=[\"res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "addons/gut/images/green.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bvrnfjkcmpr8s\"\npath=\"res://.godot/imported/green.png-e3a17091688e10a7013279b38edc7f8a.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/images/green.png\"\ndest_files=[\"res://.godot/imported/green.png-e3a17091688e10a7013279b38edc7f8a.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/images/red.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://ba2sgost7my3x\"\npath=\"res://.godot/imported/red.png-47a557c3922e800f76686bc1a4ad0c3c.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/images/red.png\"\ndest_files=[\"res://.godot/imported/red.png-47a557c3922e800f76686bc1a4ad0c3c.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/images/yellow.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://o4mo2w2ftx1v\"\npath=\"res://.godot/imported/yellow.png-b3cf3d463958a169d909273d3d742052.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/gut/images/yellow.png\"\ndest_files=[\"res://.godot/imported/yellow.png-b3cf3d463958a169d909273d3d742052.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/gut/inner_class_registry.gd",
    "content": "var _registry = {}\n\n\nfunc _create_reg_entry(base_path, subpath):\n\tvar to_return = {\n\t\t\"base_path\":base_path,\n\t\t\"subpath\":subpath,\n\t\t\"base_resource\":load(base_path),\n\t\t\"full_path\":str(\"'\", base_path, \"'\", subpath)\n\t}\n\treturn to_return\n\nfunc _register_inners(base_path, obj, prev_inner = ''):\n\tvar const_map = obj.get_script_constant_map()\n\tvar consts = const_map.keys()\n\tvar const_idx = 0\n\n\twhile(const_idx < consts.size()):\n\t\tvar key = consts[const_idx]\n\t\tvar thing = const_map[key]\n\n\t\tif(typeof(thing) == TYPE_OBJECT):\n\t\t\tvar cur_inner = str(prev_inner, \".\", key)\n\t\t\t_registry[thing] = _create_reg_entry(base_path, cur_inner)\n\t\t\t_register_inners(base_path, thing, cur_inner)\n\n\t\tconst_idx += 1\n\n\nfunc register(base_script):\n\tvar base_path = base_script.resource_path\n\t_register_inners(base_path, base_script)\n\n\nfunc get_extends_path(inner_class):\n\tif(_registry.has(inner_class)):\n\t\treturn _registry[inner_class].full_path\n\telse:\n\t\treturn null\n\n# returns the subpath for the inner class.  This includes the leading \".\" in\n# the path.\nfunc get_subpath(inner_class):\n\tif(_registry.has(inner_class)):\n\t\treturn _registry[inner_class].subpath\n\telse:\n\t\treturn ''\n\nfunc get_base_path(inner_class):\n\tif(_registry.has(inner_class)):\n\t\treturn _registry[inner_class].base_path\n\n\nfunc has(inner_class):\n\treturn _registry.has(inner_class)\n\n\nfunc get_base_resource(inner_class):\n\tif(_registry.has(inner_class)):\n\t\treturn _registry[inner_class].base_resource\n\n\nfunc to_s():\n\tvar text = \"\"\n\tfor key in _registry:\n\t\ttext += str(key, \": \", _registry[key], \"\\n\")\n\treturn text\n"
  },
  {
    "path": "addons/gut/inner_class_registry.gd.uid",
    "content": "uid://cculmuedplcov\n"
  },
  {
    "path": "addons/gut/input_factory.gd",
    "content": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# Description\n# -----------\n# ##############################################################################\n\n\n# Implemented InputEvent* convenience methods\n# \tInputEventAction\n# \tInputEventKey\n# \tInputEventMouseButton\n# \tInputEventMouseMotion\n\n# Yet to implement InputEvents\n# \tInputEventJoypadButton\n# \tInputEventJoypadMotion\n# \tInputEventMagnifyGesture\n# \tInputEventMIDI\n# \tInputEventPanGesture\n# \tInputEventScreenDrag\n# \tInputEventScreenTouch\n\n\nstatic func _to_scancode(which):\n\tvar key_code = which\n\tif(typeof(key_code) == TYPE_STRING):\n\t\tkey_code = key_code.to_upper().to_ascii_buffer()[0]\n\treturn key_code\n\n\nstatic func new_mouse_button_event(position, global_position, pressed, button_index):\n\tvar event = InputEventMouseButton.new()\n\tevent.position = position\n\tif(global_position != null):\n\t\tevent.global_position = global_position\n\tevent.pressed = pressed\n\tevent.button_index = button_index\n\n\treturn event\n\n\nstatic func key_up(which):\n\tvar event = InputEventKey.new()\n\tevent.keycode = _to_scancode(which)\n\tevent.pressed = false\n\treturn event\n\n\nstatic func key_down(which):\n\tvar event = InputEventKey.new()\n\tevent.keycode = _to_scancode(which)\n\tevent.pressed = true\n\treturn event\n\n\nstatic func action_up(which, strength=1.0):\n\tvar event  = InputEventAction.new()\n\tevent.action = which\n\tevent.strength = strength\n\treturn event\n\n\nstatic func action_down(which, strength=1.0):\n\tvar event  = InputEventAction.new()\n\tevent.action = which\n\tevent.strength = strength\n\tevent.pressed = true\n\treturn event\n\n\nstatic func mouse_left_button_down(position, global_position=null):\n\tvar event = new_mouse_button_event(position, global_position, true, MOUSE_BUTTON_LEFT)\n\treturn event\n\n\nstatic func mouse_left_button_up(position, global_position=null):\n\tvar event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_LEFT)\n\treturn event\n\n\nstatic func mouse_double_click(position, global_position=null):\n\tvar event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_LEFT)\n\tevent.double_click = true\n\treturn event\n\n\nstatic func mouse_right_button_down(position, global_position=null):\n\tvar event = new_mouse_button_event(position, global_position, true, MOUSE_BUTTON_RIGHT)\n\treturn event\n\n\nstatic func mouse_right_button_up(position, global_position=null):\n\tvar event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_RIGHT)\n\treturn event\n\n\nstatic func mouse_motion(position, global_position=null):\n\tvar event = InputEventMouseMotion.new()\n\tevent.position = position\n\tif(global_position != null):\n\t\tevent.global_position = global_position\n\treturn event\n\n\nstatic func mouse_relative_motion(offset, last_motion_event=null, speed=Vector2(0, 0)):\n\tvar event = null\n\tif(last_motion_event == null):\n\t\tevent = mouse_motion(offset)\n\t\tevent.velocity = speed\n\telse:\n\t\tevent = last_motion_event.duplicate()\n\t\tevent.position += offset\n\t\tevent.global_position += offset\n\t\tevent.relative = offset\n\t\tevent.velocity = speed\n\treturn event\n"
  },
  {
    "path": "addons/gut/input_factory.gd.uid",
    "content": "uid://dn1dj40h3vrbx\n"
  },
  {
    "path": "addons/gut/input_sender.gd",
    "content": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# Description\n# -----------\n# This class sends input to one or more recievers.  The receivers' _input,\n# _unhandled_input, and _gui_input are called sending InputEvent* events.\n# InputEvents can be sent via the helper methods or a custom made InputEvent\n# can be sent via send_event(...)\n#\n# ##############################################################################\n#extends \"res://addons/gut/input_factory.gd\"\n\n# Implemented InputEvent* convenience methods\n# \tInputEventAction\n# \tInputEventKey\n# \tInputEventMouseButton\n#\tInputEventMouseMotion\n\n# Yet to implement InputEvents\n# \tInputEventJoypadButton\n# \tInputEventJoypadMotion\n# \tInputEventMagnifyGesture\n# \tInputEventMIDI\n# \tInputEventPanGesture\n# \tInputEventScreenDrag\n# \tInputEventScreenTouch\n\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass InputQueueItem:\n\textends Node\n\n\tvar events = []\n\tvar time_delay = null\n\tvar frame_delay = null\n\tvar _waited_frames = 0\n\tvar _is_ready = false\n\tvar _delay_started = false\n\n\tsignal event_ready\n\n\t# TODO should this be done in _physics_process instead or should it be\n\t# configurable?\n\tfunc _physics_process(delta):\n\t\tif(frame_delay > 0 and _delay_started):\n\t\t\t_waited_frames += 1\n\t\t\tif(_waited_frames >= frame_delay):\n\t\t\t\tevent_ready.emit()\n\n\tfunc _init(t_delay,f_delay):\n\t\ttime_delay = t_delay\n\t\tframe_delay = f_delay\n\t\t_is_ready = time_delay == 0 and frame_delay == 0\n\n\tfunc _on_time_timeout():\n\t\t_is_ready = true\n\t\tevent_ready.emit()\n\n\tfunc _delay_timer(t):\n\t\treturn Engine.get_main_loop().root.get_tree().create_timer(t)\n\n\tfunc is_ready():\n\t\treturn _is_ready\n\n\tfunc start():\n\t\t_delay_started = true\n\t\tif(time_delay > 0):\n\t\t\tvar t = _delay_timer(time_delay)\n\t\t\tt.connect(\"timeout\",Callable(self,\"_on_time_timeout\"))\n\n\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass MouseDraw:\n\textends Node2D\n\n\tvar down_color = Color(1, 1, 1, .25)\n\tvar up_color = Color(0, 0, 0, .25)\n\tvar line_color = Color(1, 0, 0)\n\tvar disabled = true :\n\t\tget : return disabled\n\t\tset(val) :\n\t\t\tdisabled = val\n\t\t\tqueue_redraw()\n\n\tvar _draw_at = Vector2(0, 0)\n\tvar _b1_down = false\n\tvar _b2_down = false\n\n\n\tfunc draw_event(event):\n\t\tif(event is InputEventMouse):\n\t\t\t_draw_at = event.position\n\t\t\tif(event is InputEventMouseButton):\n\t\t\t\tif(event.button_index == MOUSE_BUTTON_LEFT):\n\t\t\t\t\t_b1_down = event.pressed\n\t\t\t\telif(event.button_index == MOUSE_BUTTON_RIGHT):\n\t\t\t\t\t_b2_down = event.pressed\n\t\tqueue_redraw()\n\n\n\tfunc _draw_cicled_cursor():\n\t\tvar r = 10\n\t\tvar b1_color = up_color\n\t\tvar b2_color = up_color\n\n\t\tif(_b1_down):\n\t\t\tvar pos = _draw_at - (Vector2(r * 1.5, 0))\n\t\t\tdraw_arc(pos, r / 2, 0, 360, 180, b1_color)\n\n\t\tif(_b2_down):\n\t\t\tvar pos = _draw_at + (Vector2(r * 1.5, 0))\n\t\t\tdraw_arc(pos, r / 2, 0, 360, 180, b2_color)\n\n\t\tdraw_arc(_draw_at, r, 0, 360, 360, line_color, 1)\n\t\tdraw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color)\n\t\tdraw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color)\n\n\n\tfunc _draw_square_cursor():\n\t\tvar r = 10\n\t\tvar b1_color = up_color\n\t\tvar b2_color = up_color\n\n\t\tif(_b1_down):\n\t\t\tb1_color = down_color\n\n\t\tif(_b2_down):\n\t\t\tb2_color = down_color\n\n\t\tvar blen = r * .75\n\t\t# left button rectangle\n\t\tdraw_rect(Rect2(_draw_at - Vector2(blen, blen), Vector2(blen, blen * 2)), b1_color)\n\t\t# right button rectrangle\n\t\tdraw_rect(Rect2(_draw_at - Vector2(0, blen), Vector2(blen, blen * 2)), b2_color)\n\t\t# Crosshair\n\t\tdraw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color)\n\t\tdraw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color)\n\n\n\tfunc _draw():\n\t\tif(disabled):\n\t\t\treturn\n\t\t_draw_square_cursor()\n\n\n\n\n\n\n\n# ##############################################################################\n#\n# ##############################################################################\nvar InputFactory = load(\"res://addons/gut/input_factory.gd\")\n\nconst INPUT_WARN = 'If using Input as a reciever it will not respond to *_down events until a *_up event is recieved.  Call the appropriate *_up event or use hold_for(...) to automatically release after some duration.'\n\nvar _lgr = GutUtils.get_logger()\nvar _receivers = []\nvar _input_queue = []\nvar _next_queue_item = null\n\n# used by hold_for and echo.\nvar _last_event = null\n# indexed by keycode, each entry contains a boolean value indicating the\n# last emitted \"pressed\" value for that keycode.\nvar _pressed_keys = {}\nvar _pressed_actions = {}\nvar _pressed_mouse_buttons = {}\n\nvar _auto_flush_input = false\nvar _tree_items_parent = null\nvar _mouse_draw = null;\n\nvar _default_mouse_position = {\n\tposition = Vector2(0, 0),\n\tglobal_position = Vector2(0, 0)\n}\n\nvar _last_mouse_position = {\n}\n\n\nvar mouse_warp = false\nvar draw_mouse = true\n\nsignal idle\n\n\nfunc _init(r=null):\n\tif(r != null):\n\t\tadd_receiver(r)\n\n\t_last_mouse_position = _default_mouse_position.duplicate()\n\t_tree_items_parent = Node.new()\n\tEngine.get_main_loop().root.add_child(_tree_items_parent)\n\n\t_mouse_draw = MouseDraw.new()\n\t_tree_items_parent.add_child(_mouse_draw)\n\t_mouse_draw.disabled = false\n\n\nfunc _notification(what):\n\tif(what == NOTIFICATION_PREDELETE):\n\t\tif(is_instance_valid(_tree_items_parent)):\n\t\t\t_tree_items_parent.queue_free()\n\n\nfunc _add_queue_item(item):\n\titem.connect(\"event_ready\", _on_queue_item_ready.bind(item))\n\t_next_queue_item = item\n\t_input_queue.append(item)\n\t_tree_items_parent.add_child(item)\n\tif(_input_queue.size() == 1):\n\t\titem.start()\n\n\nfunc _handle_pressed_keys(event):\n\tif(event is InputEventKey):\n\t\tif((event.pressed and !event.echo) and is_key_pressed(event.keycode)):\n\t\t\t_lgr.warn(str(\"InputSender:  key_down called for \", event.as_text(), \" when that key is already pressed.  \", INPUT_WARN))\n\t\t_pressed_keys[event.keycode] = event.pressed\n\telif(event is InputEventAction):\n\t\tif(event.pressed and is_action_pressed(event.action)):\n\t\t\t_lgr.warn(str(\"InputSender:  action_down called for \", event.action, \" when that action is already pressed.  \", INPUT_WARN))\n\t\t_pressed_actions[event.action] = event.pressed\n\telif(event is InputEventMouseButton):\n\t\tif(event.pressed and is_mouse_button_pressed(event.button_index)):\n\t\t\t_lgr.warn(str(\"InputSender:  mouse_button_down called for \", event.button_index, \" when that mouse button is already pressed.  \", INPUT_WARN))\n\t\t_pressed_mouse_buttons[event.button_index] = event\n\n\nfunc _handle_mouse_position(event):\n\tif(event is InputEventMouse):\n\t\t_mouse_draw.disabled = !draw_mouse\n\t\t_mouse_draw.draw_event(event)\n\t\tif(mouse_warp):\n\t\t\tDisplayServer.warp_mouse(event.position)\n\n\nfunc _send_event(event):\n\t_handle_mouse_position(event)\n\t_handle_pressed_keys(event)\n\n\tfor r in _receivers:\n\t\tif(r == Input):\n\t\t\tInput.parse_input_event(event)\n\t\t\tif(event is InputEventAction):\n\t\t\t\tif(event.pressed):\n\t\t\t\t\tInput.action_press(event.action)\n\t\t\t\telse:\n\t\t\t\t\tInput.action_release(event.action)\n\t\t\tif(_auto_flush_input):\n\t\t\t\tInput.flush_buffered_events()\n\t\telse:\n\t\t\tif(r.has_method(&\"_input\")):\n\t\t\t\tr._input(event)\n\n\t\t\tif(r.has_signal(&\"gui_input\")):\n\t\t\t\tr.gui_input.emit(event)\n\n\t\t\tif(r.has_method(&\"_gui_input\")):\n\t\t\t\tr._gui_input(event)\n\n\t\t\tif(r.has_method(&\"_unhandled_input\")):\n\t\t\t\tr._unhandled_input(event)\n\n\nfunc _send_or_record_event(event):\n\t_last_event = event\n\tif(_next_queue_item != null):\n\t\t_next_queue_item.events.append(event)\n\telse:\n\t\t_send_event(event)\n\n\nfunc _set_last_mouse_positions(event : InputEventMouse):\n\t_last_mouse_position.position = event.position\n\t_last_mouse_position.global_position = event.global_position\n\n\nfunc _apply_last_position_and_set_last_position(event, position, global_position):\n\tevent.position = GutUtils.nvl(position, _last_mouse_position.position)\n\tevent.global_position = GutUtils.nvl(\n\t\tglobal_position, _last_mouse_position.global_position)\n\t_set_last_mouse_positions(event)\n\n\nfunc _new_defaulted_mouse_button_event(position, global_position):\n\tvar event = InputEventMouseButton.new()\n\t_apply_last_position_and_set_last_position(event, position, global_position)\n\treturn event\n\n\nfunc _new_defaulted_mouse_motion_event(position, global_position):\n\tvar event = InputEventMouseMotion.new()\n\t_apply_last_position_and_set_last_position(event, position, global_position)\n\tfor key in _pressed_mouse_buttons:\n\t\tif(_pressed_mouse_buttons[key].pressed):\n\t\t\tevent.button_mask += key\n\treturn event\n\n\n# ------------------------------\n# Events\n# ------------------------------\nfunc _on_queue_item_ready(item):\n\tfor event in item.events:\n\t\t_send_event(event)\n\n\tvar done_event = _input_queue.pop_front()\n\tdone_event.queue_free()\n\n\tif(_input_queue.size() == 0):\n\t\t_next_queue_item = null\n\t\tidle.emit()\n\telse:\n\t\t_input_queue[0].start()\n\n\n# ------------------------------\n# Public\n# ------------------------------\nfunc add_receiver(obj):\n\t_receivers.append(obj)\n\nfunc get_receivers():\n\treturn _receivers\n\nfunc is_idle():\n\treturn _input_queue.size() == 0\n\nfunc is_key_pressed(which):\n\tvar event = InputFactory.key_up(which)\n\treturn _pressed_keys.has(event.keycode) and _pressed_keys[event.keycode]\n\nfunc is_action_pressed(which):\n\treturn _pressed_actions.has(which) and _pressed_actions[which]\n\nfunc is_mouse_button_pressed(which):\n\treturn _pressed_mouse_buttons.has(which) and _pressed_mouse_buttons[which].pressed\n\nfunc get_auto_flush_input():\n\treturn _auto_flush_input\n\nfunc set_auto_flush_input(val):\n\t_auto_flush_input = val\n\n\nfunc wait(t):\n\tif(typeof(t) == TYPE_STRING):\n\t\tvar suffix = t.substr(t.length() -1, 1)\n\t\tvar val = t.rstrip('s').rstrip('f').to_float()\n\n\t\tif(suffix.to_lower() == 's'):\n\t\t\twait_secs(val)\n\t\telif(suffix.to_lower() == 'f'):\n\t\t\twait_frames(val)\n\telse:\n\t\twait_secs(t)\n\n\treturn self\n\n\nfunc clear():\n\t_last_event = null\n\t_next_queue_item = null\n\n\tfor item in _input_queue:\n\t\titem.free()\n\t_input_queue.clear()\n\n\t_pressed_keys.clear()\n\t_pressed_actions.clear()\n\t_pressed_mouse_buttons.clear()\n\t_last_mouse_position = _default_mouse_position.duplicate()\n\n\n# ------------------------------\n# Event methods\n# ------------------------------\nfunc key_up(which):\n\tvar event = InputFactory.key_up(which)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc key_down(which):\n\tvar event = InputFactory.key_down(which)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc key_echo():\n\tif(_last_event != null and _last_event is InputEventKey):\n\t\tvar new_key = _last_event.duplicate()\n\t\tnew_key.echo = true\n\t\t_send_or_record_event(new_key)\n\treturn self\n\n\nfunc action_up(which, strength=1.0):\n\tvar event  = InputFactory.action_up(which, strength)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc action_down(which, strength=1.0):\n\tvar event  = InputFactory.action_down(which, strength)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_left_button_down(position=null, global_position=null):\n\tvar event = _new_defaulted_mouse_button_event(position, global_position)\n\tevent.pressed = true\n\tevent.button_index = MOUSE_BUTTON_LEFT\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_left_button_up(position=null, global_position=null):\n\tvar event = _new_defaulted_mouse_button_event(position, global_position)\n\tevent.pressed = false\n\tevent.button_index = MOUSE_BUTTON_LEFT\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_double_click(position=null, global_position=null):\n\tvar event = InputFactory.mouse_double_click(position, global_position)\n\tevent.double_click = true\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_right_button_down(position=null, global_position=null):\n\tvar event = _new_defaulted_mouse_button_event(position, global_position)\n\tevent.pressed = true\n\tevent.button_index = MOUSE_BUTTON_RIGHT\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_right_button_up(position=null, global_position=null):\n\tvar event = _new_defaulted_mouse_button_event(position, global_position)\n\tevent.pressed = false\n\tevent.button_index = MOUSE_BUTTON_RIGHT\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_motion(position, global_position=null):\n\tvar event = _new_defaulted_mouse_motion_event(position, global_position)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_relative_motion(offset, speed=Vector2(0, 0)):\n\tvar last_event = _new_defaulted_mouse_motion_event(null, null)\n\tvar event = InputFactory.mouse_relative_motion(offset, last_event, speed)\n\t_set_last_mouse_positions(event)\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc mouse_set_position(position, global_position=null):\n\tvar event = _new_defaulted_mouse_motion_event(position, global_position)\n\treturn self\n\n\nfunc mouse_left_click_at(where, duration = '5f'):\n\twait_frames(1)\n\tmouse_left_button_down(where)\n\thold_for(duration)\n\twait_frames(10)\n\treturn self\n\n\nfunc send_event(event):\n\t_send_or_record_event(event)\n\treturn self\n\n\nfunc release_all():\n\tfor key in _pressed_keys:\n\t\tif(_pressed_keys[key]):\n\t\t\t_send_event(InputFactory.key_up(key))\n\t_pressed_keys.clear()\n\n\tfor key in _pressed_actions:\n\t\tif(_pressed_actions[key]):\n\t\t\t_send_event(InputFactory.action_up(key))\n\t_pressed_actions.clear()\n\n\tfor key in _pressed_mouse_buttons:\n\t\tvar event = _pressed_mouse_buttons[key].duplicate()\n\t\tif(event.pressed):\n\t\t\tevent.pressed = false\n\t\t\t_send_event(event)\n\t_pressed_mouse_buttons.clear()\n\n\treturn self\n\n\nfunc wait_frames(num_frames):\n\tvar item = InputQueueItem.new(0, num_frames)\n\t_add_queue_item(item)\n\treturn self\n\n\nfunc wait_secs(num_secs):\n\tvar item = InputQueueItem.new(num_secs, 0)\n\t_add_queue_item(item)\n\treturn self\n\n\nfunc hold_for(duration):\n\tif(_last_event != null and _last_event.pressed):\n\t\tvar next_event = _last_event.duplicate()\n\t\tnext_event.pressed = false\n\n\t\twait(duration)\n\t\tsend_event(next_event)\n\treturn self\n"
  },
  {
    "path": "addons/gut/input_sender.gd.uid",
    "content": "uid://df4i0fcthmru4\n"
  },
  {
    "path": "addons/gut/junit_xml_export.gd",
    "content": "# ------------------------------------------------------------------------------\n# Creates an export of a test run in the JUnit XML format.\n# ------------------------------------------------------------------------------\nvar _exporter = GutUtils.ResultExporter.new()\n\nfunc indent(s, ind):\n\tvar to_return = ind + s\n\tto_return = to_return.replace(\"\\n\", \"\\n\" + ind)\n\treturn to_return\n\n\nfunc add_attr(name, value):\n\treturn str(name, '=\"', value, '\" ')\n\nfunc _export_test_result(test):\n\tvar to_return = ''\n\n\t# Right now the pending and failure messages won't fit in the message\n\t# attribute because they can span multiple lines and need to be escaped.\n\tif(test.status == 'pending'):\n\t\tvar skip_tag = str(\"<skipped message=\\\"pending\\\">\", test.pending[0], \"</skipped>\")\n\t\tto_return += skip_tag\n\telif(test.status == 'fail'):\n\t\tvar fail_tag = str(\"<failure message=\\\"failed\\\">\", test.failing[0], \"</failure>\")\n\t\tto_return += fail_tag\n\n\treturn to_return\n\n\nfunc _export_tests(script_result, classname):\n\tvar to_return = \"\"\n\n\tfor key in script_result.keys():\n\t\tvar test = script_result[key]\n\t\tvar assert_count = test.passing.size() + test.failing.size()\n\t\tto_return += \"<testcase \"\n\t\tto_return += add_attr(\"name\", key)\n\t\tto_return += add_attr(\"assertions\", assert_count)\n\t\tto_return += add_attr(\"status\", test.status)\n\t\tto_return += add_attr(\"classname\", classname.replace(\"res://\", \"\"))\n\t\tto_return += add_attr(\"time\", test.time_taken)\n\t\tto_return += \">\\n\"\n\n\t\tto_return += _export_test_result(test)\n\n\t\tto_return += \"</testcase>\\n\"\n\n\treturn to_return\n\nfunc _sum_test_time(script_result, classname)->float:\n\tvar to_return := 0.0\n\n\tfor key in script_result.keys():\n\t\tvar test = script_result[key]\n\t\tto_return += test.time_taken\n\n\treturn to_return\n\nfunc _export_scripts(exp_results):\n\tvar to_return = \"\"\n\tfor key in exp_results.test_scripts.scripts.keys():\n\t\tvar s = exp_results.test_scripts.scripts[key]\n\t\tto_return += \"<testsuite \"\n\t\tto_return += add_attr(\"name\", key.replace(\"res://\", \"\"))\n\t\tto_return += add_attr(\"tests\", s.props.tests)\n\t\tto_return += add_attr(\"failures\", s.props.failures)\n\t\tto_return += add_attr(\"skipped\", s.props.pending)\n\t\tto_return += add_attr(\"time\", _sum_test_time(s.tests, key) )\n\t\tto_return += \">\\n\"\n\n\t\tto_return += indent(_export_tests(s.tests, key), \"    \")\n\n\t\tto_return += \"</testsuite>\\n\"\n\n\treturn to_return\n\n\nfunc get_results_xml(gut):\n\tvar exp_results = _exporter.get_results_dictionary(gut)\n\tvar to_return = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + \"\\n\"\n\tto_return += '<testsuites '\n\tto_return += add_attr(\"name\", 'GutTests')\n\tto_return += add_attr(\"failures\", exp_results.test_scripts.props.failures)\n\tto_return += add_attr('tests', exp_results.test_scripts.props.tests)\n\tto_return += \">\\n\"\n\n\tto_return += indent(_export_scripts(exp_results), \"  \")\n\n\tto_return += '</testsuites>'\n\treturn to_return\n\n\nfunc write_file(gut, path):\n\tvar xml = get_results_xml(gut)\n\n\tvar f_result = GutUtils.write_file(path, xml)\n\tif(f_result != OK):\n\t\tvar msg = str(\"Error:  \", f_result, \".  Could not create export file \", path)\n\t\tGutUtils.get_logger().error(msg)\n\n\treturn f_result\n\n"
  },
  {
    "path": "addons/gut/junit_xml_export.gd.uid",
    "content": "uid://dfev03lfgaddb\n"
  },
  {
    "path": "addons/gut/lazy_loader.gd",
    "content": "@tool\n# ------------------------------------------------------------------------------\n# Static\n# ------------------------------------------------------------------------------\nstatic var usage_counter = load('res://addons/gut/thing_counter.gd').new()\nstatic var WarningsManager = load('res://addons/gut/warnings_manager.gd')\n\nstatic func load_all():\n\tfor key in usage_counter.things:\n\t\tkey.get_loaded()\n\n\nstatic func print_usage():\n\tfor key in usage_counter.things:\n\t\tprint(key._path, '  (', usage_counter.things[key], ')')\n\n\n# ------------------------------------------------------------------------------\n# Class\n# ------------------------------------------------------------------------------\nvar _loaded = null\nvar _path = null\n\nfunc _init(path):\n\t_path = path\n\tusage_counter.add_thing_to_count(self)\n\n\nfunc get_loaded():\n\tif(_loaded == null):\n\t\t# if(ResourceLoader.has_cached(_path)):\n\t\t# \tprint('---- already loaded ', _path, ' ----')\n\t\t# else:\n\t\t# \tprint('---- loading ', _path, ' ----')\n\t\t_loaded = WarningsManager.load_script_ignoring_all_warnings(_path)\n\tusage_counter.add(self)\n\treturn _loaded\n\n"
  },
  {
    "path": "addons/gut/lazy_loader.gd.uid",
    "content": "uid://383urtt4pkap\n"
  },
  {
    "path": "addons/gut/logger.gd",
    "content": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# This class wraps around the various printers and supplies formatting for the\n# various message types (error, warning, etc).\n# ##############################################################################\nvar types = {\n\tdebug = 'debug',\n\tdeprecated = 'deprecated',\n\terror = 'error',\n\tfailed = 'failed',\n\tinfo = 'info',\n\tnormal = 'normal',\n\torphan = 'orphan',\n\tpassed = 'passed',\n\tpending = 'pending',\n\trisky = 'risky',\n\twarn = 'warn',\n}\n\nvar fmts = {\n\tred = 'red',\n\tyellow = 'yellow',\n\tgreen = 'green',\n\n\tbold = 'bold',\n\tunderline = 'underline',\n\n\tnone = null\n}\n\nvar _type_data = {\n\ttypes.debug:\t\t{disp='DEBUG', \t\tenabled=true, fmt=fmts.bold},\n\ttypes.deprecated:\t{disp='DEPRECATED', enabled=true, fmt=fmts.none},\n\ttypes.error:\t\t{disp='ERROR', \t\tenabled=true, fmt=fmts.red},\n\ttypes.failed:\t\t{disp='Failed', \tenabled=true, fmt=fmts.red},\n\ttypes.info:\t\t\t{disp='INFO', \t\tenabled=true, fmt=fmts.bold},\n\ttypes.normal:\t\t{disp='NORMAL', \tenabled=true, fmt=fmts.none},\n\ttypes.orphan:\t\t{disp='Orphans',\tenabled=true, fmt=fmts.yellow},\n\ttypes.passed:\t\t{disp='Passed', \tenabled=true, fmt=fmts.green},\n\ttypes.pending:\t\t{disp='Pending',\tenabled=true, fmt=fmts.yellow},\n\ttypes.risky:\t\t{disp='Risky',\t\tenabled=true, fmt=fmts.yellow},\n\ttypes.warn:\t\t\t{disp='WARNING', \tenabled=true, fmt=fmts.yellow},\n}\n\nvar _logs = {\n\ttypes.warn: [],\n\ttypes.error: [],\n\ttypes.info: [],\n\ttypes.debug: [],\n\ttypes.deprecated: [],\n}\n\nvar _printers = {\n\tterminal = null,\n\tgui = null,\n\tconsole = null\n}\n\nvar _gut = null\nvar _indent_level = 0\nvar _min_indent_level = 0\nvar _indent_string = '    '\nvar _less_test_names = false\nvar _yield_calls = 0\nvar _last_yield_text = ''\n\nfunc _init():\n\t_printers.terminal = GutUtils.Printers.TerminalPrinter.new()\n\t_printers.console = GutUtils.Printers.ConsolePrinter.new()\n\t# There were some problems in the timing of disabling this at the right\n\t# time in gut_cmdln so it is disabled by default.  This is enabled\n\t# by plugin_control.gd based on settings.\n\t_printers.console.set_disabled(true)\n\nfunc get_indent_text():\n\tvar pad = ''\n\tfor i in range(_indent_level):\n\t\tpad += _indent_string\n\n\treturn pad\n\nfunc _indent_text(text):\n\tvar to_return = text\n\tvar ending_newline = ''\n\n\tif(text.ends_with(\"\\n\")):\n\t\tending_newline = \"\\n\"\n\t\tto_return = to_return.left(to_return.length() -1)\n\n\tvar pad = get_indent_text()\n\tto_return = to_return.replace(\"\\n\", \"\\n\" + pad)\n\tto_return += ending_newline\n\n\treturn pad + to_return\n\nfunc _should_print_to_printer(key_name):\n\treturn _printers[key_name] != null and !_printers[key_name].get_disabled()\n\nfunc _print_test_name():\n\tif(_gut == null):\n\t\treturn\n\n\tvar cur_test = _gut.get_current_test_object()\n\tif(cur_test == null):\n\t\treturn false\n\n\tif(!cur_test.has_printed_name):\n\t\tvar param_text = ''\n\t\tif(cur_test.arg_count > 0):\n\t\t\t# Just an FYI, parameter_handler in gut might not be set yet so can't\n\t\t\t# use it here for cooler output.\n\t\t\tparam_text = '<parameterized>'\n\t\t_output(str('* ', cur_test.name, param_text, \"\\n\"))\n\t\tcur_test.has_printed_name = true\n\nfunc _output(text, fmt=null):\n\tfor key in _printers:\n\t\tif(_should_print_to_printer(key)):\n\t\t\t_printers[key].send(text, fmt)\n\nfunc _log(text, fmt=fmts.none):\n\t_print_test_name()\n\tvar indented = _indent_text(text)\n\t_output(indented, fmt)\n\n# ---------------\n# Get Methods\n# ---------------\nfunc get_warnings():\n\treturn get_log_entries(types.warn)\n\nfunc get_errors():\n\treturn get_log_entries(types.error)\n\nfunc get_infos():\n\treturn get_log_entries(types.info)\n\nfunc get_debugs():\n\treturn get_log_entries(types.debug)\n\nfunc get_deprecated():\n\treturn get_log_entries(types.deprecated)\n\nfunc get_count(log_type=null):\n\tvar count = 0\n\tif(log_type == null):\n\t\tfor key in _logs:\n\t\t\tcount += _logs[key].size()\n\telse:\n\t\tcount = _logs[log_type].size()\n\treturn count\n\nfunc get_log_entries(log_type):\n\treturn _logs[log_type]\n\n# ---------------\n# Log methods\n# ---------------\nfunc _output_type(type, text):\n\tvar td = _type_data[type]\n\tif(!td.enabled):\n\t\treturn\n\n\t_print_test_name()\n\tif(type != types.normal):\n\t\tif(_logs.has(type)):\n\t\t\t_logs[type].append(text)\n\n\t\tvar start = str('[', td.disp, ']')\n\t\tif(text != null and text != ''):\n\t\t\tstart += ':  '\n\t\telse:\n\t\t\tstart += ' '\n\t\tvar indented_start = _indent_text(start)\n\t\tvar indented_end = _indent_text(text)\n\t\tindented_end = indented_end.lstrip(_indent_string)\n\t\t_output(indented_start, td.fmt)\n\t\t_output(indented_end + \"\\n\")\n\n\nfunc debug(text):\n\t_output_type(types.debug, text)\n\n# supply some text or the name of the deprecated method and the replacement.\nfunc deprecated(text, alt_method=null):\n\tvar msg = text\n\tif(alt_method):\n\t\tmsg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.')\n\treturn _output_type(types.deprecated, msg)\n\nfunc error(text):\n\t_output_type(types.error, text)\n\tif(_gut != null):\n\t\t_gut._fail_for_error(text)\n\nfunc failed(text):\n\t_output_type(types.failed, text)\n\nfunc info(text):\n\t_output_type(types.info, text)\n\nfunc orphan(text):\n\t_output_type(types.orphan, text)\n\nfunc passed(text):\n\t_output_type(types.passed, text)\n\nfunc pending(text):\n\t_output_type(types.pending, text)\n\nfunc risky(text):\n\t_output_type(types.risky, text)\n\nfunc warn(text):\n\t_output_type(types.warn, text)\n\nfunc log(text='', fmt=fmts.none):\n\tend_yield()\n\tif(text == ''):\n\t\t_output(\"\\n\")\n\telse:\n\t\t_log(text + \"\\n\", fmt)\n\treturn null\n\nfunc lograw(text, fmt=fmts.none):\n\treturn _output(text, fmt)\n\n# Print the test name if we aren't skipping names of tests that pass (basically\n# what _less_test_names means))\nfunc log_test_name():\n\t# suppress output if we haven't printed the test name yet and\n\t# what to print is the test name.\n\tif(!_less_test_names):\n\t\t_print_test_name()\n\n# ---------------\n# Misc\n# ---------------\nfunc get_gut():\n\treturn _gut\n\nfunc set_gut(gut):\n\t_gut = gut\n\tif(_gut == null):\n\t\t_printers.gui = null\n\telse:\n\t\tif(_printers.gui == null):\n\t\t\t_printers.gui = GutUtils.Printers.GutGuiPrinter.new()\n\n\nfunc get_indent_level():\n\treturn _indent_level\n\nfunc set_indent_level(indent_level):\n\t_indent_level = max(_min_indent_level, indent_level)\n\nfunc get_indent_string():\n\treturn _indent_string\n\nfunc set_indent_string(indent_string):\n\t_indent_string = indent_string\n\nfunc clear():\n\tfor key in _logs:\n\t\t_logs[key].clear()\n\nfunc inc_indent():\n\t_indent_level += 1\n\nfunc dec_indent():\n\t_indent_level = max(_min_indent_level, _indent_level -1)\n\nfunc is_type_enabled(type):\n\treturn _type_data[type].enabled\n\nfunc set_type_enabled(type, is_enabled):\n\t_type_data[type].enabled = is_enabled\n\nfunc get_less_test_names():\n\treturn _less_test_names\n\nfunc set_less_test_names(less_test_names):\n\t_less_test_names = less_test_names\n\nfunc disable_printer(name, is_disabled):\n\tif(_printers[name] != null):\n\t\t_printers[name].set_disabled(is_disabled)\n\nfunc is_printer_disabled(name):\n\treturn _printers[name].get_disabled()\n\nfunc disable_formatting(is_disabled):\n\tfor key in _printers:\n\t\t_printers[key].set_format_enabled(!is_disabled)\n\nfunc disable_all_printers(is_disabled):\n\tfor p in _printers:\n\t\tdisable_printer(p, is_disabled)\n\nfunc get_printer(printer_key):\n\treturn _printers[printer_key]\n\nfunc _yield_text_terminal(text):\n\tvar printer = _printers['terminal']\n\tif(_yield_calls != 0):\n\t\tprinter.clear_line()\n\t\tprinter.back(_last_yield_text.length())\n\tprinter.send(text, fmts.yellow)\n\nfunc _end_yield_terminal():\n\tvar printer = _printers['terminal']\n\tprinter.clear_line()\n\tprinter.back(_last_yield_text.length())\n\nfunc _yield_text_gui(text):\n\tpass\n\t# var lbl = _gut.get_gui().get_waiting_label()\n\t# lbl.visible = true\n\t# lbl.set_bbcode('[color=yellow]' + text + '[/color]')\n\nfunc _end_yield_gui():\n\tpass\n\t# var lbl = _gut.get_gui().get_waiting_label()\n\t# lbl.visible = false\n\t# lbl.set_text('')\n\n# This is used for displaying the \"yield detected\" and \"yielding to\" messages.\nfunc yield_msg(text):\n\tif(_type_data.warn.enabled):\n\t\tself.log(text, fmts.yellow)\n\n# This is used for the animated \"waiting\" message\nfunc yield_text(text):\n\t_yield_text_terminal(text)\n\t_yield_text_gui(text)\n\t_last_yield_text = text\n\t_yield_calls += 1\n\n# This is used for the animated \"waiting\" message\nfunc end_yield():\n\tif(_yield_calls == 0):\n\t\treturn\n\t_end_yield_terminal()\n\t_end_yield_gui()\n\t_yield_calls = 0\n\t_last_yield_text = ''\n\nfunc get_gui_bbcode():\n\treturn _printers.gui.get_bbcode()\n"
  },
  {
    "path": "addons/gut/logger.gd.uid",
    "content": "uid://byuihwq7ypii1\n"
  },
  {
    "path": "addons/gut/method_maker.gd",
    "content": "class CallParameters:\n\tvar p_name = null\n\tvar default = null\n\n\tfunc _init(n,d):\n\t\tp_name = n\n\t\tdefault = d\n\n\n# ------------------------------------------------------------------------------\n# This class will generate method declaration lines based on method meta\n# data.  It will create defaults that match the method data.\n#\n# --------------------\n# function meta data\n# --------------------\n# name:\n# flags:\n# args: [{\n# \t(class_name:),\n# \t(hint:0),\n# \t(hint_string:),\n# \t(name:),\n# \t(type:4),\n# \t(usage:7)\n# }]\n# default_args []\n\nvar _lgr = GutUtils.get_logger()\nvar default_vararg_arg_count = 10\nconst PARAM_PREFIX = 'p_'\n\n# ------------------------------------------------------\n# _supported_defaults\n#\n# This array contains all the data types that are supported for default values.\n# If a value is supported it will contain either an empty string or a prefix\n# that should be used when setting the parameter default value.\n# For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false)\n# but things like Vectors and Colors do since only the parameters to create a\n# new Vector or Color are included in the metadata.\n# ------------------------------------------------------\n\t# TYPE_NIL = 0 — Variable is of type nil (only applied for null).\n\t# TYPE_BOOL = 1 — Variable is of type bool.\n\t# TYPE_INT = 2 — Variable is of type int.\n\t# TYPE_FLOAT = 3 — Variable is of type float/real.\n\t# TYPE_STRING = 4 — Variable is of type String.\n\t# TYPE_VECTOR2 = 5 — Variable is of type Vector2.\n\t# TYPE_RECT2 = 6 — Variable is of type Rect2.\n\t# TYPE_VECTOR3 = 7 — Variable is of type Vector3.\n\t# TYPE_COLOR = 14 — Variable is of type Color.\n\t# TYPE_OBJECT = 17 — Variable is of type Object.\n\t# TYPE_DICTIONARY = 18 — Variable is of type Dictionary.\n\t# TYPE_ARRAY = 19 — Variable is of type Array.\n\t# TYPE_PACKED_VECTOR2_ARRAY = 24 — Variable is of type PackedVector2Array.\n\t# TYPE_TRANSFORM3D = 13 — Variable is of type Transform3D.\n\t# TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D.\n\t# TYPE_RID = 16 — Variable is of type RID.\n\t# TYPE_PACKED_INT32_ARRAY = 21 — Variable is of type PackedInt32Array.\n\t# TYPE_PACKED_FLOAT32_ARRAY = 22 — Variable is of type PackedFloat32Array.\n\t# TYPE_PACKED_STRING_ARRAY = 23 — Variable is of type PackedStringArray.\n\n\n# TYPE_PLANE = 9 — Variable is of type Plane.\n# TYPE_QUATERNION = 10 — Variable is of type Quaternion.\n# TYPE_AABB = 11 — Variable is of type AABB.\n# TYPE_BASIS = 12 — Variable is of type Basis.\n# TYPE_NODE_PATH = 15 — Variable is of type NodePath.\n# TYPE_PACKED_BYTE_ARRAY = 20 — Variable is of type PackedByteArray.\n# TYPE_PACKED_VECTOR3_ARRAY = 25 — Variable is of type PackedVector3Array.\n# TYPE_PACKED_COLOR_ARRAY = 26 — Variable is of type PackedColorArray.\n# TYPE_MAX = 27 — Marker for end of type constants.\n# ------------------------------------------------------\nvar _supported_defaults = []\n\nfunc _init():\n\tfor _i in range(TYPE_MAX):\n\t\t_supported_defaults.append(null)\n\n\t# These types do not require a prefix for defaults\n\t_supported_defaults[TYPE_NIL] = ''\n\t_supported_defaults[TYPE_BOOL] = ''\n\t_supported_defaults[TYPE_INT] = ''\n\t_supported_defaults[TYPE_FLOAT] = ''\n\t_supported_defaults[TYPE_OBJECT] = ''\n\t_supported_defaults[TYPE_ARRAY] = ''\n\t_supported_defaults[TYPE_STRING] = ''\n\t_supported_defaults[TYPE_STRING_NAME] = ''\n\t_supported_defaults[TYPE_DICTIONARY] = ''\n\t_supported_defaults[TYPE_PACKED_VECTOR2_ARRAY] = ''\n\t_supported_defaults[TYPE_RID] = ''\n\n\t# These require a prefix for whatever default is provided\n\t_supported_defaults[TYPE_VECTOR2] = 'Vector2'\n\t_supported_defaults[TYPE_VECTOR2I] = 'Vector2i'\n\t_supported_defaults[TYPE_RECT2] = 'Rect2'\n\t_supported_defaults[TYPE_RECT2I] = 'Rect2i'\n\t_supported_defaults[TYPE_VECTOR3] = 'Vector3'\n\t_supported_defaults[TYPE_COLOR] = 'Color'\n\t_supported_defaults[TYPE_TRANSFORM2D] = 'Transform2D'\n\t_supported_defaults[TYPE_TRANSFORM3D] = 'Transform3D'\n\t_supported_defaults[TYPE_PACKED_INT32_ARRAY] = 'PackedInt32Array'\n\t_supported_defaults[TYPE_PACKED_FLOAT32_ARRAY] = 'PackedFloat32Array'\n\t_supported_defaults[TYPE_PACKED_STRING_ARRAY] = 'PackedStringArray'\n\n# ###############\n# Private\n# ###############\nvar _func_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/function_template.txt')\nvar _init_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/init_template.txt')\n\nfunc _is_supported_default(type_flag):\n\treturn type_flag >= 0 and type_flag < _supported_defaults.size() and _supported_defaults[type_flag] != null\n\n\nfunc _make_stub_default(method, index):\n\treturn str('__gutdbl.default_val(\"', method, '\",', index, ')')\n\n\nfunc _make_arg_array(method_meta, override_size):\n\tvar to_return = []\n\n\tvar has_unsupported_defaults = false\n\n\tfor i in range(method_meta.args.size()):\n\t\tvar pname = method_meta.args[i].name\n\t\tvar dflt_text = _make_stub_default(method_meta.name, i)\n\t\tto_return.append(CallParameters.new(PARAM_PREFIX + pname, dflt_text))\n\n\tvar extra_params = GutUtils.nvl(override_size, 0)\n\tif(extra_params == 0):\n\t\tif(method_meta.flags & METHOD_FLAG_VARARG):\n\t\t\textra_params = default_vararg_arg_count\n\n\t# Add in extra parameters from stub settings.\n\tif(extra_params > 0):\n\t\tfor i in range(method_meta.args.size(), extra_params):\n\t\t\tvar pname = str(PARAM_PREFIX, 'arg', i)\n\t\t\tvar dflt_text = _make_stub_default(method_meta.name, i)\n\t\t\tto_return.append(CallParameters.new(pname, dflt_text))\n\n\treturn [has_unsupported_defaults, to_return];\n\n\n# Creates a list of parameters with defaults of null unless a default value is\n# found in the metadata.  If a default is found in the meta then it is used if\n# it is one we know how support.\n#\n# If a default is found that we don't know how to handle then this method will\n# return null.\nfunc _get_arg_text(arg_array):\n\tvar text = ''\n\n\tfor i in range(arg_array.size()):\n\t\ttext += str(arg_array[i].p_name, '=', arg_array[i].default)\n\t\tif(i != arg_array.size() -1):\n\t\t\ttext += ', '\n\n\treturn text\n\n\n# creates a call to the function in meta in the super's class.\nfunc _get_super_call_text(method_name, args):\n\tvar params = ''\n\tfor i in range(args.size()):\n\t\tparams += args[i].p_name\n\t\tif(i != args.size() -1):\n\t\t\tparams += ', '\n\n\treturn str('await super(', params, ')')\n\n\nfunc _get_spy_call_parameters_text(args):\n\tvar called_with = 'null'\n\n\tif(args.size() > 0):\n\t\tcalled_with = '['\n\t\tfor i in range(args.size()):\n\t\t\tcalled_with += args[i].p_name\n\t\t\tif(i < args.size() - 1):\n\t\t\t\tcalled_with += ', '\n\t\tcalled_with += ']'\n\n\treturn called_with\n\n\n# ###############\n# Public\n# ###############\n\nfunc _get_init_text(meta, args, method_params, param_array):\n\tvar text = null\n\n\tvar decleration = str('func ', meta.name, '(', method_params, ')')\n\tvar super_params = ''\n\tif(args.size() > 0):\n\t\tfor i in range(args.size()):\n\t\t\tsuper_params += args[i].p_name\n\t\t\tif(i != args.size() -1):\n\t\t\t\tsuper_params += ', '\n\n\ttext = _init_text.format({\n\t\t\"func_decleration\":decleration,\n\t\t\"super_params\":super_params,\n\t\t\"param_array\":param_array,\n\t\t\"method_name\":meta.name,\n\t})\n\n\treturn text\n\n\n# Creates a delceration for a function based off of function metadata.  All\n# types whose defaults are supported will have their values.  If a datatype\n# is not supported and the parameter has a default, a warning message will be\n# printed and the declaration will return null.\nfunc get_function_text(meta, override_size=null):\n\tvar method_params = ''\n\tvar text = null\n\tvar result = _make_arg_array(meta, override_size)\n\tvar has_unsupported = result[0]\n\tvar args = result[1]\n\tvar vararg_warning = \"\"\n\n\tvar param_array = _get_spy_call_parameters_text(args)\n\tif(has_unsupported):\n\t\t# This will cause a runtime error.  This is the most convenient way to\n\t\t# to stop running before the error gets more obscure.  _make_arg_array\n\t\t# generates a gut error when unsupported defaults are found.\n\t\tmethod_params = null\n\telse:\n\t\tmethod_params = _get_arg_text(args);\n\n\tif(param_array == 'null'):\n\t\tparam_array = '[]'\n\n\tif(meta.flags & METHOD_FLAG_VARARG and override_size == null):\n\t\tvararg_warning = \"__gutdbl.vararg_warning()\\n\\t\"\n\n\tif(method_params != null):\n\t\tif(meta.name == '_init'):\n\t\t\ttext =  _get_init_text(meta, args, method_params, param_array)\n\t\telse:\n\t\t\tvar decleration = str('func ', meta.name, '(', method_params, '):')\n\t\t\ttext = _func_text.format({\n\t\t\t\t\"func_decleration\":decleration,\n\t\t\t\t\"method_name\":meta.name,\n\t\t\t\t\"param_array\":param_array,\n\t\t\t\t\"super_call\":_get_super_call_text(meta.name, args),\n\t\t\t\t\"vararg_warning\":vararg_warning,\n\t\t\t})\n\n\treturn text\n\n\nfunc get_logger():\n\treturn _lgr\n\n\nfunc set_logger(logger):\n\t_lgr = logger\n"
  },
  {
    "path": "addons/gut/method_maker.gd.uid",
    "content": "uid://3dfc18qrvynr\n"
  },
  {
    "path": "addons/gut/one_to_many.gd",
    "content": "# ------------------------------------------------------------------------------\n# This datastructure represents a simple one-to-many relationship.  It manages\n# a dictionary of value/array pairs.  It ignores duplicates of both the \"one\"\n# and the \"many\".\n# ------------------------------------------------------------------------------\nvar _items = {}\n\n# return the size of _items or the size of an element in _items if \"one\" was\n# specified.\nfunc size(one=null):\n\tvar to_return = 0\n\tif(one == null):\n\t\tto_return = _items.size()\n\telif(_items.has(one)):\n\t\tto_return = _items[one].size()\n\treturn to_return\n\n# Add an element to \"one\" if it does not already exist\nfunc add(one, many_item):\n\tif(_items.has(one) and !_items[one].has(many_item)):\n\t\t_items[one].append(many_item)\n\telse:\n\t\t_items[one] = [many_item]\n\nfunc clear():\n\t_items.clear()\n\nfunc has(one, many_item):\n\tvar to_return = false\n\tif(_items.has(one)):\n\t\tto_return = _items[one].has(many_item)\n\treturn to_return\n\nfunc to_s():\n\tvar to_return = ''\n\tfor key in _items:\n\t\tto_return += str(key, \":  \", _items[key], \"\\n\")\n\treturn to_return\n"
  },
  {
    "path": "addons/gut/one_to_many.gd.uid",
    "content": "uid://cqevwrpe1er23\n"
  },
  {
    "path": "addons/gut/orphan_counter.gd",
    "content": "# ------------------------------------------------------------------------------\n# This is used to track the change in orphans over different intervals.\n# You use this by adding a counter at the start of an interval and then\n# using get_orphans_since to find out how many orphans have been created since\n# that counter was added.\n#\n# For example, when a test starts, gut adds a counter for \"test\" which\n# creates/sets the counter's value to the current orphan count.  At the end of\n# the test GUT uses get_orphans_since(\"test\") to find out how many orphans\n# were created by the test.\n# ------------------------------------------------------------------------------\nvar _counters = {}\n\nfunc orphan_count():\n\treturn Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)\n\nfunc add_counter(name):\n\t_counters[name] = orphan_count()\n\n# Returns the number of orphans created since add_counter was last called for\n# the name.  Returns -1 to avoid blowing up with an invalid name but still\n# be somewhat visible that we've done something wrong.\nfunc get_orphans_since(name):\n\treturn orphan_count() - _counters[name] if _counters.has(name) else -1\n\nfunc get_count(name):\n\treturn _counters.get(name, -1)\n\nfunc print_orphans(name, lgr):\n\tvar count = get_orphans_since(name)\n\n\tif(count > 0):\n\t\tvar o = 'orphan'\n\t\tif(count > 1):\n\t\t\to = 'orphans'\n\t\tlgr.orphan(str(count, ' new ', o, ' in ', name, '.'))\n\nfunc print_all():\n\tvar msg = str(\"Total Orphans \", orphan_count(), \"\\n\", JSON.stringify(_counters, \"    \"))\n\tprint(msg)\n\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2024 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# This is a utility for tracking changes in the orphan count.  Each time\n# add_counter is called it adds/resets the value in the dictionary to the\n# current number of orphans.  Each call to get_counter will return the change\n# in orphans since add_counter was last called.\n# ##############################################################################"
  },
  {
    "path": "addons/gut/orphan_counter.gd.uid",
    "content": "uid://cmdu0wfm6ocsh\n"
  },
  {
    "path": "addons/gut/parameter_factory.gd",
    "content": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# This is the home for all parameter creation helpers.  These functions should\n# all return an array of values to be used as parameters for parameterized\n# tests.\n# ##############################################################################\n\n# ------------------------------------------------------------------------------\n# Creates an array of dictionaries.  It pairs up the names array with each set\n# of values in values.  If more names than values are specified then the missing\n# values will be filled with nulls.  If more values than names are specified\n# those values will be ignored.\n#\n# Example:\n# \tcreate_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) returns\n#\t\t[{a:1, b:2}, {a:'one', b:'two'}]\n#\n# \tThis allows you to increase readability of your parameterized tests:\n#\tvar params = create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']])\n#\tfunc test_foo(p = use_parameters(params)):\n#\t\tassert_eq(p.a, p.b)\n#\n# Parameters:\n# \tnames:  an array of names to be used as keys in the dictionaries\n#   values:  an array of arrays of values.\n# ------------------------------------------------------------------------------\nstatic func named_parameters(names, values):\n\tvar named = []\n\tfor i in range(values.size()):\n\t\tvar entry = {}\n\n\t\tvar parray = values[i]\n\t\tif(typeof(parray) != TYPE_ARRAY):\n\t\t\tparray = [values[i]]\n\n\t\tfor j in range(names.size()):\n\t\t\tif(j >= parray.size()):\n\t\t\t\tentry[names[j]] = null\n\t\t\telse:\n\t\t\t\tentry[names[j]] = parray[j]\n\t\tnamed.append(entry)\n\n\treturn named\n\n# Additional Helper Ideas\n# * File.  IDK what it would look like.  csv maybe.\n# * Random values within a range?\n# * All int values in a range or add an optioanal step.\n# *\n"
  },
  {
    "path": "addons/gut/parameter_factory.gd.uid",
    "content": "uid://ctv0245akwsru\n"
  },
  {
    "path": "addons/gut/parameter_handler.gd",
    "content": "var _params = null\nvar _call_count = 0\nvar _logger = null\n\nfunc _init(params=null):\n\t_params = params\n\t_logger = GutUtils.get_logger()\n\tif(typeof(_params) != TYPE_ARRAY):\n\t\t_logger.error('You must pass an array to parameter_handler constructor.')\n\t\t_params = null\n\n\nfunc next_parameters():\n\t_call_count += 1\n\treturn _params[_call_count -1]\n\nfunc get_current_parameters():\n\treturn _params[_call_count]\n\nfunc is_done():\n\tvar done = true\n\tif(_params != null):\n\t\tdone = _call_count == _params.size()\n\treturn done\n\nfunc get_logger():\n\treturn _logger\n\nfunc set_logger(logger):\n\t_logger = logger\n\nfunc get_call_count():\n\treturn _call_count\n\nfunc get_parameter_count():\n\treturn _params.size()\n"
  },
  {
    "path": "addons/gut/parameter_handler.gd.uid",
    "content": "uid://1c225w8uklng\n"
  },
  {
    "path": "addons/gut/plugin.cfg",
    "content": "[plugin]\n\nname=\"Gut\"\ndescription=\"Unit Testing tool for Godot.\"\nauthor=\"Butch Wesley\"\nversion=\"9.3.0\"\nscript=\"gut_plugin.gd\"\n"
  },
  {
    "path": "addons/gut/printers.gd",
    "content": "# ------------------------------------------------------------------------------\n# Interface and some basic functionality for all printers.\n# ------------------------------------------------------------------------------\nclass Printer:\n\tvar _format_enabled = true\n\tvar _disabled = false\n\tvar _printer_name = 'NOT SET'\n\tvar _show_name = false # used for debugging, set manually\n\n\tfunc get_format_enabled():\n\t\treturn _format_enabled\n\n\tfunc set_format_enabled(format_enabled):\n\t\t_format_enabled = format_enabled\n\n\tfunc send(text, fmt=null):\n\t\tif(_disabled):\n\t\t\treturn\n\n\t\tvar formatted = text\n\t\tif(fmt != null and _format_enabled):\n\t\t\tformatted = format_text(text, fmt)\n\n\t\tif(_show_name):\n\t\t\tformatted = str('(', _printer_name, ')') + formatted\n\n\t\t_output(formatted)\n\n\tfunc get_disabled():\n\t\treturn _disabled\n\n\tfunc set_disabled(disabled):\n\t\t_disabled = disabled\n\n\t# --------------------\n\t# Virtual Methods (some have some default behavior)\n\t# --------------------\n\tfunc _output(text):\n\t\tpass\n\n\tfunc format_text(text, fmt):\n\t\treturn text\n\n# ------------------------------------------------------------------------------\n# Responsible for sending text to a GUT gui.\n# ------------------------------------------------------------------------------\nclass GutGuiPrinter:\n\textends Printer\n\tvar _textbox = null\n\n\tvar _colors = {\n\t\t\tred = Color.RED,\n\t\t\tyellow = Color.YELLOW,\n\t\t\tgreen = Color.GREEN\n\t}\n\n\tfunc _init():\n\t\t_printer_name = 'gui'\n\n\tfunc _wrap_with_tag(text, tag):\n\t\treturn str('[', tag, ']', text, '[/', tag, ']')\n\n\tfunc _color_text(text, c_word):\n\t\treturn '[color=' + c_word + ']' + text + '[/color]'\n\n\t# Remember, we have to use push and pop because the output from the tests\n\t# can contain [] in it which can mess up the formatting.  There is no way\n\t# as of 3.4 that you can get the bbcode out of RTL when using push and pop.\n\t#\n\t# The only way we could get around this is by adding in non-printable\n\t# whitespace after each \"[\" that is in the text.  Then we could maybe do\n\t# this another way and still be able to get the bbcode out, or generate it\n\t# at the same time in a buffer (like we tried that one time).\n\t#\n\t# Since RTL doesn't have good search and selection methods, and those are\n\t# really handy in the editor, it isn't worth making bbcode that can be used\n\t# there as well.\n\t#\n\t# You'll try to get it so the colors can be the same in the editor as they\n\t# are in the output.  Good luck, and I hope I typed enough to not go too\n\t# far that rabbit hole before finding out it's not worth it.\n\tfunc format_text(text, fmt):\n\t\tif(_textbox == null):\n\t\t\treturn\n\n\t\tif(fmt == 'bold'):\n\t\t\t_textbox.push_bold()\n\t\telif(fmt == 'underline'):\n\t\t\t_textbox.push_underline()\n\t\telif(_colors.has(fmt)):\n\t\t\t_textbox.push_color(_colors[fmt])\n\t\telse:\n\t\t\t# just pushing something to pop.\n\t\t\t_textbox.push_normal()\n\n\t\t_textbox.add_text(text)\n\t\t_textbox.pop()\n\n\t\treturn ''\n\n\tfunc _output(text):\n\t\tif(_textbox == null):\n\t\t\treturn\n\n\t\t_textbox.add_text(text)\n\n\tfunc get_textbox():\n\t\treturn _textbox\n\n\tfunc set_textbox(textbox):\n\t\t_textbox = textbox\n\n\t# This can be very very slow when the box has a lot of text.\n\tfunc clear_line():\n\t\t_textbox.remove_line(_textbox.get_line_count() - 1)\n\t\t_textbox.queue_redraw()\n\n\tfunc get_bbcode():\n\t\treturn _textbox.text\n\n\tfunc get_disabled():\n\t\treturn _disabled and _textbox != null\n\n# ------------------------------------------------------------------------------\n# This AND TerminalPrinter should not be enabled at the same time since it will\n# result in duplicate output.  printraw does not print to the console so i had\n# to make another one.\n# ------------------------------------------------------------------------------\nclass ConsolePrinter:\n\textends Printer\n\tvar _buffer = ''\n\n\tfunc _init():\n\t\t_printer_name = 'console'\n\n\t# suppresses output until it encounters a newline to keep things\n\t# inline as much as possible.\n\tfunc _output(text):\n\t\tif(text.ends_with(\"\\n\")):\n\t\t\tprint(_buffer + text.left(text.length() -1))\n\t\t\t_buffer = ''\n\t\telse:\n\t\t\t_buffer += text\n\n# ------------------------------------------------------------------------------\n# Prints text to terminal, formats some words.\n# ------------------------------------------------------------------------------\nclass TerminalPrinter:\n\textends Printer\n\n\tvar escape = PackedByteArray([0x1b]).get_string_from_ascii()\n\tvar cmd_colors  = {\n\t\tred = escape + '[31m',\n\t\tyellow = escape + '[33m',\n\t\tgreen = escape + '[32m',\n\n\t\tunderline = escape + '[4m',\n\t\tbold = escape + '[1m',\n\n\t\tdefault = escape + '[0m',\n\n\t\tclear_line = escape + '[2K'\n\t}\n\n\tfunc _init():\n\t\t_printer_name = 'terminal'\n\n\tfunc _output(text):\n\t\t# Note, printraw does not print to the console.\n\t\tprintraw(text)\n\n\tfunc format_text(text, fmt):\n\t\treturn cmd_colors[fmt] + text + cmd_colors.default\n\n\tfunc clear_line():\n\t\tsend(cmd_colors.clear_line)\n\n\tfunc back(n):\n\t\tsend(escape + str('[', n, 'D'))\n\n\tfunc forward(n):\n\t\tsend(escape + str('[', n, 'C'))\n"
  },
  {
    "path": "addons/gut/printers.gd.uid",
    "content": "uid://dancyagxdnux7\n"
  },
  {
    "path": "addons/gut/result_exporter.gd",
    "content": "# ------------------------------------------------------------------------------\n# Creates a structure that contains all the data about the results of running\n# tests.  This was created to make an intermediate step organizing the result\n# of a run and exporting it in a specific format.  This can also serve as a\n# unofficial GUT export format.\n# ------------------------------------------------------------------------------\nvar json = JSON.new()\n\nfunc _export_tests(collected_script):\n\tvar to_return = {}\n\tvar tests = collected_script.tests\n\tfor test in tests:\n\t\tif(test.get_status_text() != GutUtils.TEST_STATUSES.NOT_RUN):\n\t\t\tto_return[test.name] = {\n\t\t\t\t\"status\":test.get_status_text(),\n\t\t\t\t\"passing\":test.pass_texts,\n\t\t\t\t\"failing\":test.fail_texts,\n\t\t\t\t\"pending\":test.pending_texts,\n\t\t\t\t\"orphans\":test.orphans,\n\t\t\t\t\"time_taken\": test.time_taken\n\t\t\t}\n\n\treturn to_return\n\n# TODO\n#\terrors\nfunc _export_scripts(collector):\n\tif(collector == null):\n\t\treturn {}\n\n\tvar scripts = {}\n\n\tfor s in collector.scripts:\n\t\tvar test_data = _export_tests(s)\n\t\tscripts[s.get_full_name()] = {\n\t\t\t'props':{\n\t\t\t\t\"tests\":test_data.keys().size(),\n\t\t\t\t\"pending\":s.get_pending_count(),\n\t\t\t\t\"failures\":s.get_fail_count(),\n\t\t\t},\n\t\t\t\"tests\":test_data\n\t\t}\n\treturn scripts\n\nfunc _make_results_dict():\n\tvar result =  {\n\t\t'test_scripts':{\n\t\t\t\"props\":{\n\t\t\t\t\"pending\":0,\n\t\t\t\t\"failures\":0,\n\t\t\t\t\"passing\":0,\n\t\t\t\t\"tests\":0,\n\t\t\t\t\"time\":0,\n\t\t\t\t\"orphans\":0,\n\t\t\t\t\"errors\":0,\n\t\t\t\t\"warnings\":0\n\t\t\t},\n\t\t\t\"scripts\":[]\n\t\t}\n\t}\n\treturn result\n\n\nfunc get_results_dictionary(gut, include_scripts=true):\n\tvar scripts = []\n\n\tif(include_scripts):\n\t\tscripts = _export_scripts(gut.get_test_collector())\n\n\tvar result =  _make_results_dict()\n\n\tvar totals = gut.get_summary().get_totals()\n\n\tvar props = result.test_scripts.props\n\tprops.pending = totals.pending\n\tprops.failures = totals.failing\n\tprops.passing = totals.passing_tests\n\tprops.tests = totals.tests\n\tprops.errors = gut.logger.get_errors().size()\n\tprops.warnings = gut.logger.get_warnings().size()\n\tprops.time =  gut.get_elapsed_time()\n\tprops.orphans = gut.get_orphan_counter().get_orphans_since('pre_run')\n\tresult.test_scripts.scripts = scripts\n\n\treturn result\n\n\nfunc write_json_file(gut, path):\n\tvar dict = get_results_dictionary(gut)\n\tvar json_text = JSON.stringify(dict, ' ')\n\n\tvar f_result = GutUtils.write_file(path, json_text)\n\tif(f_result != OK):\n\t\tvar msg = str(\"Error:  \", f_result, \".  Could not create export file \", path)\n\t\tGutUtils.get_logger().error(msg)\n\n\treturn f_result\n\n\n\nfunc write_summary_file(gut, path):\n\tvar dict = get_results_dictionary(gut, false)\n\tvar json_text = JSON.stringify(dict, ' ')\n\n\tvar f_result = GutUtils.write_file(path, json_text)\n\tif(f_result != OK):\n\t\tvar msg = str(\"Error:  \", f_result, \".  Could not create export file \", path)\n\t\tGutUtils.get_logger().error(msg)\n\n\treturn f_result\n"
  },
  {
    "path": "addons/gut/result_exporter.gd.uid",
    "content": "uid://7s011x5x5wgi\n"
  },
  {
    "path": "addons/gut/script_parser.gd",
    "content": "# These methods didn't have flags that would exclude them from being used\n# in a double and they appear to break things if they are included.\nconst BLACKLIST = [\n\t'get_script',\n\t'has_method',\n]\n\n\n# ------------------------------------------------------------------------------\n# Combins the meta for the method with additional information.\n# * flag for whether the method is local\n# * adds a 'default' property to all parameters that can be easily checked per\n#   parameter\n# ------------------------------------------------------------------------------\nclass ParsedMethod:\n\tconst NO_DEFAULT = '__no__default__'\n\n\tvar _meta = {}\n\tvar meta = _meta :\n\t\tget: return _meta\n\t\tset(val): return;\n\n\tvar is_local = false\n\tvar _parameters = []\n\n\tfunc _init(metadata):\n\t\t_meta = metadata\n\t\tvar start_default = _meta.args.size() - _meta.default_args.size()\n\t\tfor i in range(_meta.args.size()):\n\t\t\tvar arg = _meta.args[i]\n\t\t\t# Add a \"default\" property to the metadata so we don't have to do\n\t\t\t# weird default paramter position math again.\n\t\t\tif(i >= start_default):\n\t\t\t\targ['default'] = _meta.default_args[start_default - i]\n\t\t\telse:\n\t\t\t\targ['default'] = NO_DEFAULT\n\t\t\t_parameters.append(arg)\n\n\n\tfunc is_eligible_for_doubling():\n\t\tvar has_bad_flag = _meta.flags & \\\n\t\t\t(METHOD_FLAG_OBJECT_CORE | METHOD_FLAG_VIRTUAL | METHOD_FLAG_STATIC)\n\t\treturn !has_bad_flag and BLACKLIST.find(_meta.name) == -1\n\n\n\tfunc is_accessor():\n\t\treturn _meta.name.begins_with('@') and \\\n\t\t\t(_meta.name.ends_with('_getter') or _meta.name.ends_with('_setter'))\n\n\n\tfunc to_s():\n\t\tvar s = _meta.name + \"(\"\n\n\t\tfor i in range(_meta.args.size()):\n\t\t\tvar arg = _meta.args[i]\n\t\t\tif(str(arg.default) != NO_DEFAULT):\n\t\t\t\tvar val = str(arg.default)\n\t\t\t\tif(val == ''):\n\t\t\t\t\tval = '\"\"'\n\t\t\t\ts += str(arg.name, ' = ', val)\n\t\t\telse:\n\t\t\t\ts += str(arg.name)\n\n\t\t\tif(i != _meta.args.size() -1):\n\t\t\t\ts += ', '\n\n\t\ts += \")\"\n\t\treturn s\n\n\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nclass ParsedScript:\n\t# All methods indexed by name.\n\tvar _methods_by_name = {}\n\n\tvar _script_path = null\n\tvar script_path = _script_path :\n\t\tget: return _script_path\n\t\tset(val): return;\n\n\tvar _subpath = null\n\tvar subpath = null :\n\t\tget: return _subpath\n\t\tset(val): return;\n\n\tvar _resource = null\n\tvar resource = null :\n\t\tget: return _resource\n\t\tset(val): return;\n\n\n\tvar _is_native = false\n\tvar is_native = _is_native:\n\t\tget: return _is_native\n\t\tset(val): return;\n\n\tvar _native_methods = {}\n\tvar _native_class_name = \"\"\n\n\n\n\tfunc _init(script_or_inst, inner_class=null):\n\t\tvar to_load = script_or_inst\n\n\t\tif(GutUtils.is_native_class(to_load)):\n\t\t\t_resource = to_load\n\t\t\t_is_native = true\n\t\t\tvar inst = to_load.new()\n\t\t\t_native_class_name = inst.get_class()\n\t\t\t_native_methods = inst.get_method_list()\n\t\t\tinst.free()\n\t\telse:\n\t\t\tif(!script_or_inst is Resource):\n\t\t\t\tto_load = load(script_or_inst.get_script().get_path())\n\n\t\t\t_script_path = to_load.resource_path\n\t\t\tif(inner_class != null):\n\t\t\t\t_subpath = _find_subpath(to_load, inner_class)\n\n\t\t\tif(inner_class == null):\n\t\t\t\t_resource = to_load\n\t\t\telse:\n\t\t\t\t_resource = inner_class\n\t\t\t\tto_load = inner_class\n\n\t\t_parse_methods(to_load)\n\n\n\tfunc _print_flags(meta):\n\t\tprint(str(meta.name, ':').rpad(30), str(meta.flags).rpad(4), ' = ', GutUtils.dec2bistr(meta.flags, 10))\n\n\n\tfunc _get_native_methods(base_type):\n\t\tvar to_return = []\n\t\tif(base_type != null):\n\t\t\tvar source = str('extends ', base_type)\n\t\t\tvar inst = GutUtils.create_script_from_source(source).new()\n\t\t\tto_return = inst.get_method_list()\n\t\t\tif(! inst is RefCounted):\n\t\t\t\tinst.free()\n\t\treturn to_return\n\n\n\tfunc _parse_methods(thing):\n\t\tvar methods = []\n\t\tif(is_native):\n\t\t\tmethods = _native_methods.duplicate()\n\t\telse:\n\t\t\tvar base_type = thing.get_instance_base_type()\n\t\t\tmethods = _get_native_methods(base_type)\n\n\t\tfor m in methods:\n\t\t\tvar parsed = ParsedMethod.new(m)\n\t\t\t_methods_by_name[m.name] = parsed\n\t\t\t# _init must always be included so that we can initialize\n\t\t\t# double_tools\n\t\t\tif(m.name == '_init'):\n\t\t\t\tparsed.is_local = true\n\n\n\t\t# This loop will overwrite all entries in _methods_by_name with the local\n\t\t# method object so there is only ever one listing for a function with\n\t\t# the right \"is_local\" flag.\n\t\tif(!is_native):\n\t\t\tmethods = thing.get_script_method_list()\n\t\t\tfor m in methods:\n\t\t\t\tvar parsed_method = ParsedMethod.new(m)\n\t\t\t\tparsed_method.is_local = true\n\t\t\t\t_methods_by_name[m.name] = parsed_method\n\n\n\tfunc _find_subpath(parent_script, inner):\n\t\tvar const_map = parent_script.get_script_constant_map()\n\t\tvar consts = const_map.keys()\n\t\tvar const_idx = 0\n\t\tvar found = false\n\t\tvar to_return = null\n\n\t\twhile(const_idx < consts.size() and !found):\n\t\t\tvar key = consts[const_idx]\n\t\t\tvar const_val = const_map[key]\n\t\t\tif(typeof(const_val) == TYPE_OBJECT):\n\t\t\t\tif(const_val == inner):\n\t\t\t\t\tfound = true\n\t\t\t\t\tto_return = key\n\t\t\t\telse:\n\t\t\t\t\tto_return = _find_subpath(const_val, inner)\n\t\t\t\t\tif(to_return != null):\n\t\t\t\t\t\tto_return = str(key, '.', to_return)\n\t\t\t\t\t\tfound = true\n\n\t\t\tconst_idx += 1\n\n\t\treturn to_return\n\n\n\tfunc get_method(name):\n\t\treturn _methods_by_name[name]\n\n\n\tfunc get_super_method(name):\n\t\tvar to_return = get_method(name)\n\t\tif(to_return.is_local):\n\t\t\tto_return = null\n\n\t\treturn to_return\n\n\tfunc get_local_method(name):\n\t\tvar to_return = get_method(name)\n\t\tif(!to_return.is_local):\n\t\t\tto_return = null\n\n\t\treturn to_return\n\n\n\tfunc get_sorted_method_names():\n\t\tvar keys = _methods_by_name.keys()\n\t\tkeys.sort()\n\t\treturn keys\n\n\n\tfunc get_local_method_names():\n\t\tvar names = []\n\t\tfor method in _methods_by_name:\n\t\t\tif(_methods_by_name[method].is_local):\n\t\t\t\tnames.append(method)\n\n\t\treturn names\n\n\n\tfunc get_super_method_names():\n\t\tvar names = []\n\t\tfor method in _methods_by_name:\n\t\t\tif(!_methods_by_name[method].is_local):\n\t\t\t\tnames.append(method)\n\n\t\treturn names\n\n\n\tfunc get_local_methods():\n\t\tvar to_return = []\n\t\tfor key in _methods_by_name:\n\t\t\tvar method = _methods_by_name[key]\n\t\t\tif(method.is_local):\n\t\t\t\tto_return.append(method)\n\t\treturn to_return\n\n\n\tfunc get_super_methods():\n\t\tvar to_return = []\n\t\tfor key in _methods_by_name:\n\t\t\tvar method = _methods_by_name[key]\n\t\t\tif(!method.is_local):\n\t\t\t\tto_return.append(method)\n\t\treturn to_return\n\n\n\tfunc get_extends_text():\n\t\tvar text = null\n\t\tif(is_native):\n\t\t\ttext = str(\"extends \", _native_class_name)\n\t\telse:\n\t\t\ttext = str(\"extends '\", _script_path, \"'\")\n\t\t\tif(_subpath != null):\n\t\t\t\ttext += '.' + _subpath\n\t\treturn text\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nvar scripts = {}\n\nfunc _get_instance_id(thing):\n\tvar inst_id = null\n\n\tif(GutUtils.is_native_class(thing)):\n\t\tvar id_str = str(thing).replace(\"<\", '').replace(\">\", '').split('#')[1]\n\t\tinst_id = id_str.to_int()\n\telif(typeof(thing) == TYPE_STRING):\n\t\tif(FileAccess.file_exists(thing)):\n\t\t\tinst_id = load(thing).get_instance_id()\n\telse:\n\t\tinst_id = thing.get_instance_id()\n\n\treturn inst_id\n\n\nfunc parse(thing, inner_thing=null):\n\tvar key = -1\n\tif(inner_thing == null):\n\t\tkey = _get_instance_id(thing)\n\telse:\n\t\tkey = _get_instance_id(inner_thing)\n\n\tvar parsed = null\n\n\tif(key != null):\n\t\tif(scripts.has(key)):\n\t\t\tparsed = scripts[key]\n\t\telse:\n\t\t\tvar obj = instance_from_id(_get_instance_id(thing))\n\t\t\tvar inner = null\n\t\t\tif(inner_thing != null):\n\t\t\t\tinner = instance_from_id(_get_instance_id(inner_thing))\n\n\t\t\tif(obj is Resource or GutUtils.is_native_class(obj)):\n\t\t\t\tparsed = ParsedScript.new(obj, inner)\n\t\t\t\tscripts[key] = parsed\n\n\treturn parsed\n\n"
  },
  {
    "path": "addons/gut/script_parser.gd.uid",
    "content": "uid://14e0xjs8qi15\n"
  },
  {
    "path": "addons/gut/signal_watcher.gd",
    "content": "# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n\n# Some arbitrary string that should never show up by accident.  If it does, then\n# shame on  you.\nconst ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'\n\n# This hash holds the objects that are being watched, the signals that are being\n# watched, and an array of arrays that contains arguments that were passed\n# each time the signal was emitted.\n#\n# For example:\n#\t_watched_signals => {\n#\t\tref1 => {\n#\t\t\t'signal1' => [[], [], []],\n#\t\t\t'signal2' => [[p1, p2]],\n#\t\t\t'signal3' => [[p1]]\n#\t\t},\n#\t\tref2 => {\n#\t\t\t'some_signal' => [],\n#\t\t\t'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]]\n#\t\t}\n#\t}\n#\n# In this sample:\n#\t- signal1 on the ref1 object was emitted 3 times and each time, zero\n#\tparameters were passed.\n#\t- signal3 on ref1 was emitted once and passed a single parameter\n#\t- some_signal on ref2 was never emitted.\n#\t- other_signal on ref2 was emitted 3 times, each time with 3 parameters.\nvar _watched_signals = {}\nvar _lgr = GutUtils.get_logger()\n\nfunc _add_watched_signal(obj, name):\n\t# SHORTCIRCUIT - ignore dupes\n\tif(_watched_signals.has(obj) and _watched_signals[obj].has(name)):\n\t\treturn\n\n\tif(!_watched_signals.has(obj)):\n\t\t_watched_signals[obj] = {name:[]}\n\telse:\n\t\t_watched_signals[obj][name] = []\n\tobj.connect(name,Callable(self,'_on_watched_signal').bind(obj,name))\n\n# This handles all the signals that are watched.  It supports up to 9 parameters\n# which could be emitted by the signal and the two parameters used when it is\n# connected via watch_signal.  I chose 9 since you can only specify up to 9\n# parameters when dynamically calling a method via call (per the Godot\n# documentation, i.e. some_object.call('some_method', 1, 2, 3...)).\n#\n# Based on the documentation of emit_signal, it appears you can only pass up\n# to 4 parameters when firing a signal.  I haven't verified this, but this should\n# future proof this some if the value ever grows.\nfunc _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \\\n\t\t\t\t\t\targ4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \\\n\t\t\t\t\t\targ7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \\\n\t\t\t\t\t\targ10=ARG_NOT_SET, arg11=ARG_NOT_SET):\n\tvar args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11]\n\n\t# strip off any unused vars.\n\tvar idx = args.size() -1\n\twhile(str(args[idx]) == ARG_NOT_SET):\n\t\targs.remove_at(idx)\n\t\tidx -= 1\n\n\t# retrieve object and signal name from the array and remove_at them.  These\n\t# will always be at the end since they are added when the connect happens.\n\tvar signal_name = args[args.size() -1]\n\targs.pop_back()\n\tvar object = args[args.size() -1]\n\targs.pop_back()\n\n\tif(_watched_signals.has(object)):\n\t\t_watched_signals[object][signal_name].append(args)\n\telse:\n\t\t_lgr.error(str(\"signal_watcher._on_watched_signal:  Got signal for unwatched object:  \", object, '::', signal_name))\n\n# This parameter stuff should go into test.gd not here.  This thing works\n# just fine the way it is.\nfunc _obj_name_pair(obj_or_signal, signal_name=null):\n\tvar to_return = {\n\t\t'object' : obj_or_signal,\n\t\t'signal_name' : signal_name\n\t}\n\tif(obj_or_signal is Signal):\n\t\tto_return.object =  obj_or_signal.get_object()\n\t\tto_return.signal_name = obj_or_signal.get_name()\n\n\treturn to_return\n\n\nfunc does_object_have_signal(object, signal_name):\n\tvar signals = object.get_signal_list()\n\tfor i in range(signals.size()):\n\t\tif(signals[i]['name'] == signal_name):\n\t\t\treturn true\n\treturn false\n\nfunc watch_signals(object):\n\tvar signals = object.get_signal_list()\n\tfor i in range(signals.size()):\n\t\t_add_watched_signal(object, signals[i]['name'])\n\nfunc watch_signal(object, signal_name):\n\tvar did = false\n\tif(does_object_have_signal(object, signal_name)):\n\t\t_add_watched_signal(object, signal_name)\n\t\tdid = true\n\telse:\n\t\tGutUtils.get_logger().warn(str(object, ' does not have signal ', signal_name))\n\treturn did\n\nfunc get_emit_count(object, signal_name):\n\tvar to_return = -1\n\tif(is_watching(object, signal_name)):\n\t\tto_return = _watched_signals[object][signal_name].size()\n\treturn to_return\n\nfunc did_emit(object, signal_name=null):\n\tvar vals = _obj_name_pair(object, signal_name)\n\tvar did = false\n\tif(is_watching(vals.object, vals.signal_name)):\n\t\tdid = get_emit_count(vals.object, vals.signal_name) != 0\n\treturn did\n\nfunc print_object_signals(object):\n\tvar list = object.get_signal_list()\n\tfor i in range(list.size()):\n\t\tprint(list[i].name, \"\\n  \", list[i])\n\nfunc get_signal_parameters(object, signal_name, index=-1):\n\tvar params = null\n\tif(is_watching(object, signal_name)):\n\t\tvar all_params = _watched_signals[object][signal_name]\n\t\tif(all_params.size() > 0):\n\t\t\tif(index == -1):\n\t\t\t\tindex = all_params.size() -1\n\t\t\tparams = all_params[index]\n\treturn params\n\nfunc is_watching_object(object):\n\treturn _watched_signals.has(object)\n\nfunc is_watching(object, signal_name):\n\treturn _watched_signals.has(object) and _watched_signals[object].has(signal_name)\n\nfunc clear():\n\tfor obj in _watched_signals:\n\t\tif(GutUtils.is_not_freed(obj)):\n\t\t\tfor signal_name in _watched_signals[obj]:\n\t\t\t\tobj.disconnect(signal_name, Callable(self,'_on_watched_signal'))\n\t_watched_signals.clear()\n\n# Returns a list of all the signal names that were emitted by the object.\n# If the object is not being watched then an empty list is returned.\nfunc get_signals_emitted(obj):\n\tvar emitted = []\n\tif(is_watching_object(obj)):\n\t\tfor signal_name in _watched_signals[obj]:\n\t\t\tif(_watched_signals[obj][signal_name].size() > 0):\n\t\t\t\temitted.append(signal_name)\n\n\treturn emitted\n\n\nfunc get_signal_summary(obj):\n\tvar emitted = {}\n\tif(is_watching_object(obj)):\n\t\tfor signal_name in _watched_signals[obj]:\n\t\t\tif(_watched_signals[obj][signal_name].size() > 0):\n\t\t\t\t# maybe this could return parameters if any were sent.  should\n\t\t\t\t# have an empty list if no parameters were ever sent to the\n\t\t\t\t# signal.  Or this all just gets moved into print_signal_summary\n\t\t\t\t# since this wouldn't be that useful without more data in the\n\t\t\t\t# summary.\n\t\t\t\tvar entry = {\n\t\t\t\t\temit_count = get_emit_count(obj, signal_name)\n\t\t\t\t}\n\t\t\t\temitted[signal_name] = entry\n\n\treturn emitted\n\n\nfunc print_signal_summary(obj):\n\tif(!is_watching_object(obj)):\n\t\tvar msg = str('Not watching signals for ', obj)\n\t\tGutUtils.get_logger().warn(msg)\n\t\treturn\n\n\tvar summary = get_signal_summary(obj)\n\tprint(obj, '::Signals')\n\tvar sorted = summary.keys()\n\tsorted.sort()\n\tfor key in sorted:\n\t\tprint(' -  ', key, ' x ', summary[key].emit_count)\n"
  },
  {
    "path": "addons/gut/signal_watcher.gd.uid",
    "content": "uid://c3ooc4spo4imo\n"
  },
  {
    "path": "addons/gut/source_code_pro.fnt.import",
    "content": "[remap]\n\nimporter=\"font_data_bmfont\"\ntype=\"FontFile\"\nuid=\"uid://cfawt18qr4256\"\npath=\"res://.godot/imported/source_code_pro.fnt-042fb383b3c7b4c19e67c852f7fbefca.fontdata\"\n\n[deps]\n\nsource_file=\"res://addons/gut/source_code_pro.fnt\"\ndest_files=[\"res://.godot/imported/source_code_pro.fnt-042fb383b3c7b4c19e67c852f7fbefca.fontdata\"]\n\n[params]\n\nfallbacks=[]\ncompress=true\nscaling_mode=2\n"
  },
  {
    "path": "addons/gut/spy.gd",
    "content": "# {\n#   instance_id_or_path1:{\n#       method1:[ [p1, p2], [p1, p2] ],\n#       method2:[ [p1, p2], [p1, p2] ]\n#   },\n#   instance_id_or_path1:{\n#       method1:[ [p1, p2], [p1, p2] ],\n#       method2:[ [p1, p2], [p1, p2] ]\n#   },\n# }\nvar _calls = {}\nvar _lgr = GutUtils.get_logger()\nvar _compare = GutUtils.Comparator.new()\n\nfunc _find_parameters(call_params, params_to_find):\n\tvar found = false\n\tvar idx = 0\n\twhile(idx < call_params.size() and !found):\n\t\tvar result = _compare.deep(call_params[idx], params_to_find)\n\t\tif(result.are_equal):\n\t\t\tfound = true\n\t\telse:\n\t\t\tidx += 1\n\treturn found\n\nfunc _get_params_as_string(params):\n\tvar to_return = ''\n\tif(params == null):\n\t\treturn ''\n\n\tfor i in range(params.size()):\n\t\tif(params[i] == null):\n\t\t\tto_return += 'null'\n\t\telse:\n\t\t\tif(typeof(params[i]) == TYPE_STRING):\n\t\t\t\tto_return += str('\"', params[i], '\"')\n\t\t\telse:\n\t\t\t\tto_return += str(params[i])\n\t\tif(i != params.size() -1):\n\t\t\tto_return += ', '\n\treturn to_return\n\nfunc add_call(variant, method_name, parameters=null):\n\tif(!_calls.has(variant)):\n\t\t_calls[variant] = {}\n\n\tif(!_calls[variant].has(method_name)):\n\t\t_calls[variant][method_name] = []\n\n\t_calls[variant][method_name].append(parameters)\n\nfunc was_called(variant, method_name, parameters=null):\n\tvar to_return = false\n\tif(_calls.has(variant) and _calls[variant].has(method_name)):\n\t\tif(parameters):\n\t\t\tto_return = _find_parameters(_calls[variant][method_name], parameters)\n\t\telse:\n\t\t\tto_return = true\n\treturn to_return\n\nfunc get_call_parameters(variant, method_name, index=-1):\n\tvar to_return = null\n\tvar get_index = -1\n\n\tif(_calls.has(variant) and _calls[variant].has(method_name)):\n\t\tvar call_size = _calls[variant][method_name].size()\n\t\tif(index == -1):\n\t\t\t# get the most recent call by default\n\t\t\tget_index =  call_size -1\n\t\telse:\n\t\t\tget_index = index\n\n\t\tif(get_index < call_size):\n\t\t\tto_return = _calls[variant][method_name][get_index]\n\t\telse:\n\t\t\t_lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls:  ', call_size))\n\n\treturn to_return\n\nfunc call_count(instance, method_name, parameters=null):\n\tvar to_return = 0\n\n\tif(was_called(instance, method_name)):\n\t\tif(parameters):\n\t\t\tfor i in range(_calls[instance][method_name].size()):\n\t\t\t\tif(_calls[instance][method_name][i] == parameters):\n\t\t\t\t\tto_return += 1\n\t\telse:\n\t\t\tto_return = _calls[instance][method_name].size()\n\treturn to_return\n\nfunc clear():\n\t_calls = {}\n\nfunc get_call_list_as_string(instance):\n\tvar to_return = ''\n\tif(_calls.has(instance)):\n\t\tfor method in _calls[instance]:\n\t\t\tfor i in range(_calls[instance][method].size()):\n\t\t\t\tto_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), \")\\n\")\n\treturn to_return\n\nfunc get_logger():\n\treturn _lgr\n\nfunc set_logger(logger):\n\t_lgr = logger\n"
  },
  {
    "path": "addons/gut/spy.gd.uid",
    "content": "uid://ccp5i1o8n3tj7\n"
  },
  {
    "path": "addons/gut/strutils.gd",
    "content": "class_name GutStringUtils\n\n# Hash containing all the built in types in Godot.  This provides an English\n# name for the types that corosponds with the type constants defined in the\n# engine.\nvar types = {}\n\nfunc _init_types_dictionary():\n\ttypes[TYPE_NIL] = 'NIL'\n\ttypes[TYPE_AABB] = 'AABB'\n\ttypes[TYPE_ARRAY] = 'ARRAY'\n\ttypes[TYPE_BASIS] = 'BASIS'\n\ttypes[TYPE_BOOL] = 'BOOL'\n\ttypes[TYPE_CALLABLE] = 'CALLABLE'\n\ttypes[TYPE_COLOR] = 'COLOR'\n\ttypes[TYPE_DICTIONARY] = 'DICTIONARY'\n\ttypes[TYPE_FLOAT] = 'FLOAT'\n\ttypes[TYPE_INT] = 'INT'\n\ttypes[TYPE_MAX] = 'MAX'\n\ttypes[TYPE_NODE_PATH] = 'NODE_PATH'\n\ttypes[TYPE_OBJECT] = 'OBJECT'\n\ttypes[TYPE_PACKED_BYTE_ARRAY] = 'PACKED_BYTE_ARRAY'\n\ttypes[TYPE_PACKED_COLOR_ARRAY] = 'PACKED_COLOR_ARRAY'\n\ttypes[TYPE_PACKED_FLOAT32_ARRAY] = 'PACKED_FLOAT32_ARRAY'\n\ttypes[TYPE_PACKED_FLOAT64_ARRAY] = 'PACKED_FLOAT64_ARRAY'\n\ttypes[TYPE_PACKED_INT32_ARRAY] = 'PACKED_INT32_ARRAY'\n\ttypes[TYPE_PACKED_INT64_ARRAY] = 'PACKED_INT64_ARRAY'\n\ttypes[TYPE_PACKED_STRING_ARRAY] = 'PACKED_STRING_ARRAY'\n\ttypes[TYPE_PACKED_VECTOR2_ARRAY] = 'PACKED_VECTOR2_ARRAY'\n\ttypes[TYPE_PACKED_VECTOR3_ARRAY] = 'PACKED_VECTOR3_ARRAY'\n\ttypes[TYPE_PLANE] = 'PLANE'\n\ttypes[TYPE_PROJECTION] = 'PROJECTION'\n\ttypes[TYPE_QUATERNION] = 'QUATERNION'\n\ttypes[TYPE_RECT2] = 'RECT2'\n\ttypes[TYPE_RECT2I] = 'RECT2I'\n\ttypes[TYPE_RID] = 'RID'\n\ttypes[TYPE_SIGNAL] = 'SIGNAL'\n\ttypes[TYPE_STRING_NAME] = 'STRING_NAME'\n\ttypes[TYPE_STRING] = 'STRING'\n\ttypes[TYPE_TRANSFORM2D] = 'TRANSFORM2D'\n\ttypes[TYPE_TRANSFORM3D] = 'TRANSFORM3D'\n\ttypes[TYPE_VECTOR2] = 'VECTOR2'\n\ttypes[TYPE_VECTOR2I] = 'VECTOR2I'\n\ttypes[TYPE_VECTOR3] = 'VECTOR3'\n\ttypes[TYPE_VECTOR3I] = 'VECTOR3I'\n\ttypes[TYPE_VECTOR4] = 'VECTOR4'\n\ttypes[TYPE_VECTOR4I] = 'VECTOR4I'\n\n# Types to not be formatted when using _str\nvar _str_ignore_types = [\n\tTYPE_INT, TYPE_FLOAT, TYPE_STRING,\n\tTYPE_NIL, TYPE_BOOL\n]\n\nfunc _init():\n\t_init_types_dictionary()\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _get_filename(path):\n\treturn path.split('/')[-1]\n\n# ------------------------------------------------------------------------------\n# Gets the filename of an object passed in.  This does not return the\n# full path to the object, just the filename.\n# ------------------------------------------------------------------------------\nfunc _get_obj_filename(thing):\n\tvar filename = null\n\n\tif(thing == null or\n\t\tGutUtils.is_native_class(thing) or\n\t\t!is_instance_valid(thing) or\n\t\tstr(thing) == '<Object#null>' or\n\t\ttypeof(thing) != TYPE_OBJECT or\n\t\tGutUtils.is_double(thing)):\n\t\treturn\n\n\tif(thing.get_script() == null):\n\t\tif(thing is PackedScene):\n\t\t\tfilename = _get_filename(thing.resource_path)\n\t\telse:\n\t\t\t# If it isn't a packed scene and it doesn't have a script then\n\t\t\t# we do nothing.  This just reads better.\n\t\t\tpass\n\telif(!GutUtils.is_native_class(thing)):\n\t\tvar dict = inst_to_dict(thing)\n\t\tfilename = _get_filename(dict['@path'])\n\t\tif(str(dict['@subpath']) != ''):\n\t\t\tfilename += str('/', dict['@subpath'])\n\n\treturn filename\n\n# ------------------------------------------------------------------------------\n# Better object/thing to string conversion.  Includes extra details about\n# whatever is passed in when it can/should.\n# ------------------------------------------------------------------------------\nfunc type2str(thing):\n\tvar filename = _get_obj_filename(thing)\n\tvar str_thing = str(thing)\n\n\tif(thing == null):\n\t\t# According to str there is a difference between null and an Object\n\t\t# that is somehow null.  To avoid getting '[Object:null]' as output\n\t\t# always set it to str(null) instead of str(thing).  A null object\n\t\t# will pass typeof(thing) == TYPE_OBJECT check so this has to be\n\t\t# before that.\n\t\tstr_thing = str(null)\n\telif(typeof(thing) == TYPE_FLOAT):\n\t\tif(!'.' in str_thing):\n\t\t\tstr_thing += '.0'\n\telif(typeof(thing) == TYPE_STRING):\n\t\tstr_thing = str('\"', thing, '\"')\n\telif(typeof(thing) in _str_ignore_types):\n\t\t# do nothing b/c we already have str(thing) in\n\t\t# to_return.  I think this just reads a little\n\t\t# better this way.\n\t\tpass\n\telif(typeof(thing) == TYPE_OBJECT):\n\t\tif(GutUtils.is_native_class(thing)):\n\t\t\tstr_thing = GutUtils.get_native_class_name(thing)\n\t\telif(GutUtils.is_double(thing)):\n\t\t\tvar double_path = _get_filename(thing.__gutdbl.thepath)\n\t\t\tif(thing.__gutdbl.subpath != ''):\n\t\t\t\tdouble_path += str('/', thing.__gutdbl.subpath)\n\t\t\telif(thing.__gutdbl.from_singleton != ''):\n\t\t\t\tdouble_path = thing.__gutdbl.from_singleton + \" Singleton\"\n\n\t\t\tvar double_type = \"double\"\n\t\t\tif(thing.__gutdbl.is_partial):\n\t\t\t\tdouble_type = \"partial-double\"\n\n\t\t\tstr_thing += str(\"(\", double_type, \" of \", double_path, \")\")\n\n\t\t\tfilename = null\n\telif(types.has(typeof(thing))):\n\t\tif(!str_thing.begins_with('(')):\n\t\t\tstr_thing = '(' + str_thing + ')'\n\t\tstr_thing = str(types[typeof(thing)], str_thing)\n\n\tif(filename != null):\n\t\tstr_thing += str('(', filename, ')')\n\treturn str_thing\n\n# ------------------------------------------------------------------------------\n# Returns the string truncated with an '...' in it.  Shows the start and last\n# 10 chars.  If the string is  smaller than max_size the entire string is\n# returned.  If max_size is -1 then truncation is skipped.\n# ------------------------------------------------------------------------------\nfunc truncate_string(src, max_size):\n\tvar to_return = src\n\tif(src.length() > max_size - 10 and max_size != -1):\n\t\tto_return = str(src.substr(0, max_size - 10), '...',  src.substr(src.length() - 10, src.length()))\n\treturn to_return\n\n\nfunc _get_indent_text(times, pad):\n\tvar to_return = ''\n\tfor i in range(times):\n\t\tto_return += pad\n\n\treturn to_return\n\nfunc indent_text(text, times, pad):\n\tif(times == 0):\n\t\treturn text\n\n\tvar to_return = text\n\tvar ending_newline = ''\n\n\tif(text.ends_with(\"\\n\")):\n\t\tending_newline = \"\\n\"\n\t\tto_return = to_return.left(to_return.length() -1)\n\n\tvar padding = _get_indent_text(times, pad)\n\tto_return = to_return.replace(\"\\n\", \"\\n\" + padding)\n\tto_return += ending_newline\n\n\treturn padding + to_return\n"
  },
  {
    "path": "addons/gut/strutils.gd.uid",
    "content": "uid://c38kiuljuuy2y\n"
  },
  {
    "path": "addons/gut/stub_params.gd",
    "content": "\nvar _lgr = GutUtils.get_logger()\nvar logger = _lgr :\n\tget: return _lgr\n\tset(val): _lgr = val\n\nvar return_val = null\nvar stub_target = null\n# the parameter values to match method call on.\nvar parameters = null\nvar stub_method = null\nvar call_super = false\nvar call_this = null\n# Whether this is a stub for default parameter values as they are defined in\n# the script, and not an overridden default value.\nvar is_script_default = false\n\n# -- Paramter Override --\n# Parmater overrides are stored in here along with all the other stub info\n# so that you can chain stubbing parameter overrides along with all the\n# other stubbing.  This adds some complexity to the logic that tries to\n# find the correct stub for a call by a double.  Since an instance of this\n# class could be just a parameter override, or it could have been chained\n# we have to have _paramter_override_only so that we know when to tell the\n# difference.\nvar parameter_count = -1\nvar parameter_defaults = null\n# Anything that would make this stub not just an override of paramters\n# must set this flag to false.  This must be private bc the actual logic\n# to determine if this stub is only an override is more complicated.\nvar _parameter_override_only = true\n# --\n\nconst NOT_SET = '|_1_this_is_not_set_1_|'\n\nfunc _init(target=null, method=null, _subpath=null):\n\tstub_target = target\n\tstub_method = method\n\n\tif(typeof(target) == TYPE_CALLABLE):\n\t\tstub_target = target.get_object()\n\t\tstub_method = target.get_method()\n\t\tparameters = target.get_bound_arguments()\n\t\tif(parameters.size() == 0):\n\t\t\tparameters = null\n\telif(typeof(target) == TYPE_STRING):\n\t\tif(target.is_absolute_path()):\n\t\t\tstub_target = load(str(target))\n\t\telse:\n\t\t\t_lgr.warn(str(target, ' is not a valid path'))\n\n\tif(stub_target is PackedScene):\n\t\tstub_target = GutUtils.get_scene_script_object(stub_target)\n\n\t# this is used internally to stub default parameters for everything that is\n\t# doubled...or something.  Look for stub_defaults_from_meta for usage.  This\n\t# behavior is not to be used by end users.\n\tif(typeof(method) == TYPE_DICTIONARY):\n\t\t_load_defaults_from_metadata(method)\n\n\nfunc _load_defaults_from_metadata(meta):\n\tstub_method = meta.name\n\tvar values = meta.default_args.duplicate()\n\twhile (values.size() < meta.args.size()):\n\t\tvalues.push_front(null)\n\n\tparam_defaults(values)\n\n\nfunc to_return(val):\n\tif(stub_method == '_init'):\n\t\t_lgr.error(\"You cannot stub _init to do nothing.  Super's _init is always called.\")\n\telse:\n\t\treturn_val = val\n\t\tcall_super = false\n\t\t_parameter_override_only = false\n\treturn self\n\n\nfunc to_do_nothing():\n\tto_return(null)\n\treturn self\n\n\nfunc to_call_super():\n\tcall_super = true\n\t_parameter_override_only = false\n\treturn self\n\n\nfunc to_call(callable : Callable):\n\tcall_this = callable\n\treturn self\n\n\nfunc when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET):\n\tparameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10]\n\tvar idx = 0\n\twhile(idx < parameters.size()):\n\t\tif(str(parameters[idx]) == NOT_SET):\n\t\t\tparameters.remove_at(idx)\n\t\telse:\n\t\t\tidx += 1\n\treturn self\n\n\nfunc param_count(x):\n\tparameter_count = x\n\treturn self\n\n\nfunc param_defaults(values):\n\tparameter_count = values.size()\n\tparameter_defaults = values\n\treturn self\n\n\nfunc has_param_override():\n\treturn parameter_count != -1\n\n\nfunc is_param_override_only():\n\tvar ret_val = false\n\tif(has_param_override()):\n\t\tret_val = _parameter_override_only\n\treturn ret_val\n\n\nfunc to_s():\n\tvar base_string = str(stub_target, '.', stub_method)\n\n\tif(has_param_override()):\n\t\tbase_string += str(' (param count override=', parameter_count, ' defaults=', parameter_defaults)\n\t\tif(is_param_override_only()):\n\t\t\tbase_string += \" ONLY\"\n\t\tif(is_script_default):\n\t\t\tbase_string += \" script default\"\n\t\tbase_string += ') '\n\n\tif(call_super):\n\t\tbase_string += \" to call SUPER\"\n\n\tif(call_this != null):\n\t\tbase_string += str(\" to call \", call_this)\n\n\tif(parameters != null):\n\t\tbase_string += str(' with params (', parameters, ') returns ', return_val)\n\telse:\n\t\tbase_string += str(' returns ', return_val)\n\n\treturn base_string\n"
  },
  {
    "path": "addons/gut/stub_params.gd.uid",
    "content": "uid://quh1a0ft8hsn\n"
  },
  {
    "path": "addons/gut/stubber.gd",
    "content": "# -------------\n# returns{} and parameters {} have the followin structure\n# -------------\n# {\n# \tinst_id_or_path1:{\n# \t\tmethod_name1: [StubParams, StubParams],\n# \t\tmethod_name2: [StubParams, StubParams]\n# \t},\n# \tinst_id_or_path2:{\n# \t\tmethod_name1: [StubParams, StubParams],\n# \t\tmethod_name2: [StubParams, StubParams]\n# \t}\n# }\nvar returns = {}\nvar _lgr = GutUtils.get_logger()\nvar _strutils = GutUtils.Strutils.new()\nvar _class_db_name_hash = {}\n\nfunc _init():\n\t_class_db_name_hash = _make_crazy_dynamic_over_engineered_class_db_hash()\n\n# So, I couldn't figure out how to get to a reference for a GDNative Class\n# using a string.  ClassDB has all thier names...so I made a hash using those\n# names and the classes.  Then I dynmaically make a script that has that as\n# the source and grab the hash out of it and return it.  Super Rube Golbergery,\n# but tons of fun.\nfunc _make_crazy_dynamic_over_engineered_class_db_hash():\n\tvar text = \"var all_the_classes: Dictionary = {\\n\"\n\tfor classname in ClassDB.get_class_list():\n\t\tif(ClassDB.can_instantiate(classname)):\n\t\t\ttext += str('\"', classname, '\": ', classname, \", \\n\")\n\t\telse:\n\t\t\ttext += str('# ', classname, \"\\n\")\n\ttext += \"}\"\n\tvar inst =  GutUtils.create_script_from_source(text).new()\n\treturn inst.all_the_classes\n\n\nfunc _find_matches(obj, method):\n\tvar matches = null\n\tvar last_not_null_parent = null\n\n\t# Search for what is passed in first.  This could be a class or an instance.\n\t# We want to find the instance before we find the class.  If we do not have\n\t# an entry for the instance then see if we have an entry for the class.\n\tif(returns.has(obj) and returns[obj].has(method)):\n\t\tmatches = returns[obj][method]\n\telif(GutUtils.is_instance(obj)):\n\t\tvar parent = obj.get_script()\n\t\tvar found = false\n\t\twhile(parent != null and !found):\n\t\t\tfound = returns.has(parent)\n\n\t\t\tif(!found):\n\t\t\t\tlast_not_null_parent = parent\n\t\t\t\tparent = parent.get_base_script()\n\n\t\t# Could not find the script so check to see if a native class of this\n\t\t# type was stubbed.\n\t\tif(!found):\n\t\t\tvar base_type = last_not_null_parent.get_instance_base_type()\n\t\t\tif(_class_db_name_hash.has(base_type)):\n\t\t\t\tparent = _class_db_name_hash[base_type]\n\t\t\t\tfound = returns.has(parent)\n\n\t\tif(found and returns[parent].has(method)):\n\t\t\tmatches = returns[parent][method]\n\n\treturn matches\n\n\n# Searches returns for an entry that matches the instance or the class that\n# passed in obj is.\n#\n# obj can be an instance, class, or a path.\nfunc _find_stub(obj, method, parameters=null, find_overloads=false):\n\tvar to_return = null\n\tvar matches = _find_matches(obj, method)\n\n\tif(matches == null):\n\t\treturn null\n\n\tvar param_match = null\n\tvar null_match = null\n\tvar overload_match = null\n\n\tfor i in range(matches.size()):\n\t\tvar cur_stub = matches[i]\n\t\tif(cur_stub.parameters == parameters):\n\t\t\tparam_match = cur_stub\n\n\t\tif(cur_stub.parameters == null and !cur_stub.is_param_override_only()):\n\t\t\tnull_match = cur_stub\n\n\t\tif(cur_stub.has_param_override()):\n\t\t\tif(overload_match == null || overload_match.is_script_default):\n\t\t\t\toverload_match = cur_stub\n\n\tif(find_overloads and overload_match != null):\n\t\tto_return = overload_match\n\t# We have matching parameter values so return the stub value for that\n\telif(param_match != null):\n\t\tto_return = param_match\n\t# We found a case where the parameters were not specified so return\n\t# parameters for that.  Only do this if the null match is not *just*\n\t# a paramerter override stub.\n\telif(null_match != null):\n\t\tto_return = null_match\n\n\treturn to_return\n\n\n\n# ##############\n# Public\n# ##############\n\nfunc add_stub(stub_params):\n\tstub_params._lgr = _lgr\n\tvar key = stub_params.stub_target\n\n\tif(!returns.has(key)):\n\t\treturns[key] = {}\n\n\tif(!returns[key].has(stub_params.stub_method)):\n\t\treturns[key][stub_params.stub_method] = []\n\n\treturns[key][stub_params.stub_method].append(stub_params)\n\n\n# Gets a stubbed return value for the object and method passed in.  If the\n# instance was stubbed it will use that, otherwise it will use the path and\n# subpath of the object to try to find a value.\n#\n# It will also use the optional list of parameter values to find a value.  If\n# the object was stubbed with no parameters than any parameters will match.\n# If it was stubbed with specific parameter values then it will try to match.\n# If the parameters do not match BUT there was also an empty parameter list stub\n# then it will return those.\n# If it cannot find anything that matches then null is returned.for\n#\n# Parameters\n# obj:  this should be an instance of a doubled object.\n# method:  the method called\n# parameters:  optional array of parameter vales to find a return value for.\nfunc get_return(obj, method, parameters=null):\n\tvar stub_info = _find_stub(obj, method, parameters)\n\n\tif(stub_info != null):\n\t\treturn stub_info.return_val\n\telse:\n\t\t_lgr.info(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '.  Null was returned.'))\n\t\treturn null\n\n\nfunc should_call_super(obj, method, parameters=null):\n\tvar stub_info = _find_stub(obj, method, parameters)\n\n\tvar is_partial = false\n\tif(typeof(obj) != TYPE_STRING): # some stubber tests test with strings\n\t\tis_partial = obj.__gutdbl.is_partial\n\tvar should = is_partial\n\n\tif(stub_info != null):\n\t\tshould = stub_info.call_super\n\telif(!is_partial):\n\t\t# this log message is here because of how the generated doubled scripts\n\t\t# are structured.  With this log msg here, you will only see one\n\t\t# \"unstubbed\" info instead of multiple.\n\t\t_lgr.info('Unstubbed call to ' + method + '::' + _strutils.type2str(obj))\n\t\tshould = false\n\n\treturn should\n\n\nfunc get_call_this(obj, method, parameters=null):\n\tvar stub_info = _find_stub(obj, method, parameters)\n\n\tif(stub_info != null):\n\t\treturn stub_info.call_this\n\n\nfunc get_parameter_count(obj, method):\n\tvar to_return = null\n\tvar stub_info = _find_stub(obj, method, null, true)\n\n\tif(stub_info != null and stub_info.has_param_override()):\n\t\tto_return = stub_info.parameter_count\n\n\treturn to_return\n\n\nfunc get_default_value(obj, method, p_index):\n\tvar to_return = null\n\tvar stub_info = _find_stub(obj, method, null, true)\n\n\tif(stub_info != null and\n\t\tstub_info.parameter_defaults != null and\n\t\tstub_info.parameter_defaults.size() > p_index):\n\n\t\tto_return = stub_info.parameter_defaults[p_index]\n\n\treturn to_return\n\n\nfunc clear():\n\treturns.clear()\n\n\nfunc get_logger():\n\treturn _lgr\n\n\nfunc set_logger(logger):\n\t_lgr = logger\n\n\nfunc to_s():\n\tvar text = ''\n\tfor thing in returns:\n\t\ttext += str(\"-- \", thing, \" --\\n\")\n\t\tfor method in returns[thing]:\n\t\t\ttext += str(\"\\t\", method, \"\\n\")\n\t\t\tfor i in range(returns[thing][method].size()):\n\t\t\t\ttext += \"\\t\\t\" + returns[thing][method][i].to_s() + \"\\n\"\n\n\tif(text == ''):\n\t\ttext = 'Stubber is empty';\n\n\treturn text\n\n\nfunc stub_defaults_from_meta(target, method_meta):\n\tvar params = GutUtils.StubParams.new(target, method_meta)\n\tparams.is_script_default = true\n\tadd_stub(params)\n"
  },
  {
    "path": "addons/gut/stubber.gd.uid",
    "content": "uid://cl4j7tddd7y0a\n"
  },
  {
    "path": "addons/gut/summary.gd",
    "content": "# ------------------------------------------------------------------------------\n# Prints things, mostly.  Knows too much about gut.gd, but it's only supposed to\n# work with gut.gd, so I'm fine with that.\n# ------------------------------------------------------------------------------\n# a _test_collector to use when one is not provided.\nvar _gut = null\n\n\nfunc _init(gut=null):\n\t_gut = gut\n\n# ---------------------\n# Private\n# ---------------------\nfunc _log_end_run_header(gut):\n\tvar lgr = gut.get_logger()\n\tlgr.log(\"\\n\\n\\n\")\n\tlgr.log('==============================================', lgr.fmts.yellow)\n\tlgr.log(\"= Run Summary\", lgr.fmts.yellow)\n\tlgr.log('==============================================', lgr.fmts.yellow)\n\n\nfunc _log_what_was_run(gut):\n\tif(!GutUtils.is_null_or_empty(gut._select_script)):\n\t\tgut.p('Ran Scripts matching \"' + gut._select_script + '\"')\n\tif(!GutUtils.is_null_or_empty(gut._unit_test_name)):\n\t\tgut.p('Ran Tests matching \"' + gut._unit_test_name + '\"')\n\tif(!GutUtils.is_null_or_empty(gut._inner_class_name)):\n\t\tgut.p('Ran Inner Classes matching \"' + gut._inner_class_name + '\"')\n\n\nfunc _log_orphans_and_disclaimer(gut):\n\tvar lgr = gut.get_logger()\n\tif(!lgr.is_type_enabled('orphan')):\n\t\treturn\n\n\tvar counter = gut.get_orphan_counter()\n\t# Do not count any of the test scripts since these will be released when GUT\n\t# is released.\n\tvar do_not_count_orphans = counter.get_count(\"pre_run\") + gut.get_test_script_count()\n\tvar total_run_orphans = counter.orphan_count() - do_not_count_orphans\n\n\tif(total_run_orphans > 0):\n\t\tlgr.orphan(str(\"Total orphans in run \", total_run_orphans))\n\t\tgut.p(\"Note:  This count does not include GUT objects that will be freed upon exit.\")\n\t\tgut.p(\"       It also does not include any orphans created by global scripts\")\n\t\tgut.p(\"       loaded before tests were ran.\")\n\t\tgut.p(str(\"Total orphans = \", counter.orphan_count()))\n\t\tgut.p('')\n\n\nfunc _total_fmt(text, value):\n\tvar space = 18\n\tif(str(value) == '0'):\n\t\tvalue = 'none'\n\treturn str(text.rpad(space), value)\n\n\nfunc _log_non_zero_total(text, value, lgr):\n\tif(str(value) != '0'):\n\t\tlgr.log(_total_fmt(text, value))\n\t\treturn 1\n\telse:\n\t\treturn 0\n\n\nfunc _log_totals(gut, totals):\n\tvar lgr = gut.get_logger()\n\tlgr.log()\n\n\tlgr.log(\"---- Totals ----\")\n\tvar issue_count = 0\n\tissue_count += _log_non_zero_total('Errors', totals.errors, lgr)\n\tissue_count += _log_non_zero_total('Warnings', totals.warnings, lgr)\n\tissue_count += _log_non_zero_total('Deprecated', totals.deprecated, lgr)\n\tif(issue_count > 0):\n\t\tlgr.log(\"\")\n\n\tlgr.log(_total_fmt( 'Scripts', totals.scripts))\n\tlgr.log(_total_fmt( 'Tests', gut.get_test_collector().get_ran_test_count()))\n\tlgr.log(_total_fmt( '  Passing', totals.passing_tests))\n\t_log_non_zero_total('  Failing', totals.failing_tests, lgr)\n\t_log_non_zero_total('  Risky/Pending', totals.risky + totals.pending, lgr)\n\tlgr.log(_total_fmt( 'Asserts', totals.passing + totals.failing))\n\tlgr.log(_total_fmt( 'Time', str(gut.get_elapsed_time(), 's')))\n\n\treturn totals\n\n\nfunc _log_nothing_run(gut):\n\tvar lgr = gut.get_logger()\n\tlgr.error(\"Nothing was run.\")\n\tlgr.log('On the one hand nothing failed, on the other hand nothing did anything.')\n\n\n# ---------------------\n# Public\n# ---------------------\nfunc log_all_non_passing_tests(gut=_gut):\n\tvar test_collector = gut.get_test_collector()\n\tvar lgr = gut.get_logger()\n\n\tvar to_return = {\n\t\tpassing = 0,\n\t\tnon_passing = 0\n\t}\n\n\tfor test_script in test_collector.scripts:\n\t\tlgr.set_indent_level(0)\n\n\t\tif(test_script.was_skipped or test_script.get_fail_count() > 0 or test_script.get_pending_count() > 0):\n\t\t\tlgr.log(\"\\n\" + test_script.get_full_name(), lgr.fmts.underline)\n\n\t\tif(test_script.was_skipped):\n\t\t\tlgr.inc_indent()\n\t\t\tvar skip_msg = str('[Risky] Script was skipped:  ', test_script.skip_reason)\n\t\t\tlgr.log(skip_msg, lgr.fmts.yellow)\n\t\t\tlgr.dec_indent()\n\n\t\tfor test in test_script.tests:\n\t\t\tif(test.was_run):\n\t\t\t\tif(test.is_passing()):\n\t\t\t\t\tto_return.passing += 1\n\t\t\t\telse:\n\t\t\t\t\tto_return.non_passing += 1\n\t\t\t\t\tlgr.log(str('- ', test.name))\n\t\t\t\t\tlgr.inc_indent()\n\n\t\t\t\t\tfor i in range(test.fail_texts.size()):\n\t\t\t\t\t\tlgr.failed(test.fail_texts[i])\n\t\t\t\t\tfor i in range(test.pending_texts.size()):\n\t\t\t\t\t\tlgr.pending(test.pending_texts[i])\n\t\t\t\t\tif(test.is_risky()):\n\t\t\t\t\t\tlgr.risky('Did not assert')\n\t\t\t\t\tlgr.dec_indent()\n\n\treturn to_return\n\n\nfunc log_the_final_line(totals, gut):\n\tvar lgr = gut.get_logger()\n\tvar grand_total_text = \"\"\n\tvar grand_total_fmt = lgr.fmts.none\n\tif(totals.failing_tests > 0):\n\t\tgrand_total_text = str(totals.failing_tests, \" failing tests\")\n\t\tgrand_total_fmt = lgr.fmts.red\n\telif(totals.risky > 0 or totals.pending > 0):\n\t\tgrand_total_text = str(totals.risky + totals.pending, \" pending/risky tests.\")\n\t\tgrand_total_fmt = lgr.fmts.yellow\n\telse:\n\t\tgrand_total_text = \"All tests passed!\"\n\t\tgrand_total_fmt = lgr.fmts.green\n\n\tlgr.log(str(\"---- \", grand_total_text, \" ----\"), grand_total_fmt)\n\n\nfunc log_totals(gut, totals):\n\tvar lgr = gut.get_logger()\n\tvar orig_indent = lgr.get_indent_level()\n\tlgr.set_indent_level(0)\n\t_log_totals(gut, totals)\n\tlgr.set_indent_level(orig_indent)\n\n\nfunc get_totals(gut=_gut):\n\tvar tc = gut.get_test_collector()\n\tvar lgr = gut.get_logger()\n\n\tvar totals = {\n\t\tfailing = 0,\n\t\tfailing_tests = 0,\n\t\tpassing = 0,\n\t\tpassing_tests = 0,\n\t\tpending = 0,\n\t\trisky = 0,\n\t\tscripts = tc.get_ran_script_count(),\n\t\ttests = 0,\n\n\t\tdeprecated = lgr.get_deprecated().size(),\n\t\terrors = lgr.get_errors().size(),\n\t\twarnings = lgr.get_warnings().size(),\n\t}\n\n\tfor s in tc.scripts:\n\t\t# assert totals\n\t\ttotals.passing += s.get_pass_count()\n\t\ttotals.pending += s.get_pending_count()\n\t\ttotals.failing += s.get_fail_count()\n\n\t\t# test totals\n\t\ttotals.tests += s.get_ran_test_count()\n\t\ttotals.passing_tests += s.get_passing_test_count()\n\t\ttotals.failing_tests += s.get_failing_test_count()\n\t\ttotals.risky += s.get_risky_count()\n\n\treturn totals\n\n\nfunc log_end_run(gut=_gut):\n\tvar totals = get_totals(gut)\n\tif(totals.tests == 0):\n\t\t_log_nothing_run(gut)\n\t\treturn\n\n\t_log_end_run_header(gut)\n\tvar lgr = gut.get_logger()\n\n\tlog_all_non_passing_tests(gut)\n\tlog_totals(gut, totals)\n\tlgr.log(\"\\n\")\n\n\t_log_orphans_and_disclaimer(gut)\n\t_log_what_was_run(gut)\n\tlog_the_final_line(totals, gut)\n\tlgr.log(\"\")\n"
  },
  {
    "path": "addons/gut/summary.gd.uid",
    "content": "uid://c4koq4xiyamds\n"
  },
  {
    "path": "addons/gut/test.gd",
    "content": "class_name GutTest\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2020 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n# View readme for usage details.\n#\n# Version - see gut.gd\n# ##############################################################################\n# Class that all test scripts must extend.`\n#\n# This provides all the asserts and other testing features.  Test scripts are\n# run by the Gut class in gut.gd\n# ##############################################################################\nextends Node\n\nvar _compare = GutUtils.Comparator.new()\n\n\n# Need a reference to the instance that is running the tests.  This\n# is set by the gut class when it runs the test script.\nvar gut: GutMain = null\n\nvar _disable_strict_datatype_checks = false\n# Holds all the text for a test's fail/pass.  This is used for testing purposes\n# to see the text of a failed sub-test in test_test.gd\nvar _fail_pass_text = []\n\nconst EDITOR_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE | PROPERTY_USAGE_DEFAULT\nconst VARIABLE_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE\n\n# Summary counts for the test.\nvar _summary = {\n\tasserts = 0,\n\tpassed = 0,\n\tfailed = 0,\n\ttests = 0,\n\tpending = 0\n}\n\n# This is used to watch signals so we can make assertions about them.\nvar _signal_watcher = load('res://addons/gut/signal_watcher.gd').new()\n\n# Convenience copy of GutUtils.DOUBLE_STRATEGY\nvar DOUBLE_STRATEGY = GutUtils.DOUBLE_STRATEGY\n\nvar _lgr = GutUtils.get_logger()\nvar _strutils = GutUtils.Strutils.new()\nvar _awaiter = null\n\n# syntax sugar\nvar ParameterFactory = GutUtils.ParameterFactory\nvar CompareResult = GutUtils.CompareResult\nvar InputFactory = GutUtils.InputFactory\nvar InputSender = GutUtils.InputSender\n\n\n\nvar _was_ready_called = false\n# I haven't decided if we should be using _ready or not.  Right now gut.gd will\n# call this if _ready was not called (because it was overridden without a super\n# call).  Maybe gut.gd should just call _do_ready_stuff (after we rename it to\n# something better).  I'm leaving all this as it is until it bothers me more.\nfunc _do_ready_stuff():\n\t_awaiter = GutUtils.Awaiter.new()\n\tadd_child(_awaiter)\n\t_was_ready_called = true\n\n\nfunc _ready():\n\t_do_ready_stuff()\n\n\nfunc _notification(what):\n\t# Tests are never expected to re-enter the tree.  Tests are removed from the\n\t# tree after they are run.\n\tif(what == NOTIFICATION_EXIT_TREE):\n\t\t_awaiter.queue_free()\n\n\nfunc _str(thing):\n\treturn _strutils.type2str(thing)\n\n\nfunc _str_precision(value, precision):\n\tvar to_return = _str(value)\n\tvar format = str('%.', precision, 'f')\n\tif(typeof(value) == TYPE_FLOAT):\n\t\tto_return = format % value\n\telif(typeof(value) == TYPE_VECTOR2):\n\t\tto_return = str('VECTOR2(', format % value.x, ', ', format %value.y, ')')\n\telif(typeof(value) == TYPE_VECTOR3):\n\t\tto_return = str('VECTOR3(', format % value.x, ', ', format %value.y, ', ', format % value.z, ')')\n\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# Fail an assertion.  Causes test and script to fail as well.\n# ------------------------------------------------------------------------------\nfunc _fail(text):\n\t_summary.asserts += 1\n\t_summary.failed += 1\n\t_fail_pass_text.append('failed:  ' + text)\n\tif(gut):\n\t\t_lgr.failed(gut.get_call_count_text() + text)\n\t\tgut._fail(text)\n\n\n# ------------------------------------------------------------------------------\n# Pass an assertion.\n# ------------------------------------------------------------------------------\nfunc _pass(text):\n\t_summary.asserts += 1\n\t_summary.passed += 1\n\t_fail_pass_text.append('passed:  ' + text)\n\tif(gut):\n\t\t_lgr.passed(text)\n\t\tgut._pass(text)\n\n# ------------------------------------------------------------------------------\n# Checks if the datatypes passed in match.  If they do not then this will cause\n# a fail to occur.  If they match then TRUE is returned, FALSE if not.  This is\n# used in all the assertions that compare values.\n# ------------------------------------------------------------------------------\nfunc _do_datatypes_match__fail_if_not(got, expected, text):\n\tvar did_pass = true\n\n\tif(!_disable_strict_datatype_checks):\n\t\tvar got_type = typeof(got)\n\t\tvar expect_type = typeof(expected)\n\t\tif(got_type != expect_type and got != null and expected != null):\n\t\t\t# If we have a mismatch between float and int (types 2 and 3) then\n\t\t\t# print out a warning but do not fail.\n\t\t\tif([2, 3].has(got_type) and [2, 3].has(expect_type)):\n\t\t\t\t_lgr.warn(str('Warn:  Float/Int comparison.  Got ', _strutils.types[got_type],\n\t\t\t\t\t' but expected ', _strutils.types[expect_type]))\n\t\t\telif([TYPE_STRING, TYPE_STRING_NAME].has(got_type) and [TYPE_STRING, TYPE_STRING_NAME].has(expect_type)):\n\t\t\t\tpass\n\t\t\telse:\n\t\t\t\t_fail('Cannot compare ' + _strutils.types[got_type] + '[' + _str(got) + '] to ' + \\\n\t\t\t\t\t_strutils.types[expect_type] + '[' + _str(expected) + '].  ' + text)\n\t\t\t\tdid_pass = false\n\n\treturn did_pass\n\n# ------------------------------------------------------------------------------\n# Create a string that lists all the methods that were called on an spied\n# instance.\n# ------------------------------------------------------------------------------\nfunc _get_desc_of_calls_to_instance(inst):\n\tvar BULLET = '  * '\n\tvar calls = gut.get_spy().get_call_list_as_string(inst)\n\t# indent all the calls\n\tcalls = BULLET + calls.replace(\"\\n\", \"\\n\" + BULLET)\n\t# remove_at trailing newline and bullet\n\tcalls = calls.substr(0, calls.length() - BULLET.length() - 1)\n\treturn \"Calls made on \" + str(inst) + \"\\n\" + calls\n\n# ------------------------------------------------------------------------------\n# Signal assertion helper.  Do not call directly, use _can_make_signal_assertions\n# ------------------------------------------------------------------------------\nfunc _fail_if_does_not_have_signal(object, signal_name):\n\tvar did_fail = false\n\tif(!_signal_watcher.does_object_have_signal(object, signal_name)):\n\t\t_fail(str('Object ', object, ' does not have the signal [', signal_name, ']'))\n\t\tdid_fail = true\n\treturn did_fail\n\n# ------------------------------------------------------------------------------\n# Signal assertion helper.  Do not call directly, use _can_make_signal_assertions\n# ------------------------------------------------------------------------------\nfunc _fail_if_not_watching(object):\n\tvar did_fail = false\n\tif(!_signal_watcher.is_watching_object(object)):\n\t\t_fail(str('Cannot make signal assertions because the object ', object, \\\n\t\t\t\t' is not being watched.  Call watch_signals(some_object) to be able to make assertions about signals.'))\n\t\tdid_fail = true\n\treturn did_fail\n\n# ------------------------------------------------------------------------------\n# Returns text that contains original text and a list of all the signals that\n# were emitted for the passed in object.\n# ------------------------------------------------------------------------------\nfunc _get_fail_msg_including_emitted_signals(text, object):\n\treturn str(text,\" (Signals emitted: \", _signal_watcher.get_signals_emitted(object), \")\")\n\n# ------------------------------------------------------------------------------\n# This validates that parameters is an array and generates a specific error\n# and a failure with a specific message\n# ------------------------------------------------------------------------------\nfunc _fail_if_parameters_not_array(parameters):\n\tvar invalid = parameters != null and typeof(parameters) != TYPE_ARRAY\n\tif(invalid):\n\t\t_lgr.error('The \"parameters\" parameter must be an array of expected parameter values.')\n\t\t_fail('Cannot compare paramter values because an array was not passed.')\n\treturn invalid\n\n\n# ------------------------------------------------------------------------------\n# A bunch of common checkes used when validating a double/method pair.  If\n# everything is ok then an empty string is returned, otherwise the message\n# is returned.\n# ------------------------------------------------------------------------------\nfunc _get_bad_double_or_method_message(inst, method_name, what_you_cant_do):\n\tvar to_return = ''\n\n\tif(!GutUtils.is_double(inst)):\n\t\tto_return = str(\"An instance of a Double was expected, you passed:  \", _str(inst))\n\telif(!inst.has_method(method_name)):\n\t\tto_return = str(\"You cannot \", what_you_cant_do, \" [\", method_name, \"] because the method does not exist.  \",\n\t\t\t\"This can happen if the method is virtual and not overloaded (i.e. _ready) \",\n\t\t\t\"or you have mistyped the name of the method.\")\n\telif(!inst.__gutdbl_values.doubled_methods.has(method_name)):\n\t\tto_return = str(\"You cannot \", what_you_cant_do, \" [\", method_name, \"] because \",\n\t\t\t_str(inst), ' does not overload it or it was ignored with ',\n\t\t\t'ignore_method_when_doubling.  See Doubling ',\n\t\t\t'Strategy in the wiki for details on including non-overloaded ',\n\t\t\t'methods in a double.')\n\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _fail_if_not_double_or_does_not_have_method(inst, method_name):\n\tvar to_return = OK\n\n\tvar msg = _get_bad_double_or_method_message(inst, method_name, 'spy on')\n\tif(msg != ''):\n\t\t_fail(msg)\n\t\tto_return = ERR_INVALID_DATA\n\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _create_obj_from_type(type):\n\tvar obj = null\n\tif type.is_class(\"PackedScene\"):\n\t\tobj = type.instantiate()\n\t\tadd_child(obj)\n\telse:\n\t\tobj = type.new()\n\treturn obj\n\n\n# #######################\n# Virtual Methods\n# #######################\n\nfunc should_skip_script():\n\treturn false\n\n\nfunc before_all():\n\tpass\n\n\nfunc before_each():\n\tpass\n\n\nfunc after_all():\n\tpass\n\n\nfunc after_each():\n\tpass\n\n# #######################\n# Public\n# #######################\n\nfunc get_logger():\n\treturn _lgr\n\nfunc set_logger(logger):\n\t_lgr = logger\n\n\n# #######################\n# Asserts\n# #######################\n\n# ------------------------------------------------------------------------------\n# Asserts that the expected value equals the value got.\n# ------------------------------------------------------------------------------\nfunc assert_eq(got, expected, text=\"\"):\n\n\tif(_do_datatypes_match__fail_if_not(got, expected, text)):\n\t\tvar disp = \"[\" + _str(got) + \"] expected to equal [\" + _str(expected) + \"]:  \" + text\n\t\tvar result = null\n\n\t\tresult = _compare.simple(got, expected)\n\n\t\tif(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]):\n\t\t\tdisp = str(result.summary, '  ', text)\n\t\t\t_lgr.info('Array/Dictionary compared by value.  Use assert_same to compare references.  Use assert_eq_deep to see diff when failing.')\n\n\t\tif(result.are_equal):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n\n# ------------------------------------------------------------------------------\n# Asserts that the value got does not equal the \"not expected\" value.\n# ------------------------------------------------------------------------------\nfunc assert_ne(got, not_expected, text=\"\"):\n\tif(_do_datatypes_match__fail_if_not(got, not_expected, text)):\n\t\tvar disp = \"[\" + _str(got) + \"] expected to not equal [\" + _str(not_expected) + \"]:  \" + text\n\t\tvar result = null\n\n\t\tresult = _compare.simple(got, not_expected)\n\n\t\tif(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]):\n\t\t\tdisp = str(result.summary, '  ', text)\n\t\t\t_lgr.info('Array/Dictionary compared by value.  Use assert_not_same to compare references.  Use assert_ne_deep to see diff.')\n\n\t\tif(result.are_equal):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that the expected value almost equals the value got.\n# ------------------------------------------------------------------------------\nfunc assert_almost_eq(got, expected, error_interval, text=''):\n\tvar disp = \"[\" + _str_precision(got, 20) + \"] expected to equal [\" + _str(expected) + \"] +/- [\" + str(error_interval) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)):\n\t\tif not _is_almost_eq(got, expected, error_interval):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that the expected value does not almost equal the value got.\n# ------------------------------------------------------------------------------\nfunc assert_almost_ne(got, not_expected, error_interval, text=''):\n\tvar disp = \"[\" + _str_precision(got, 20) + \"] expected to not equal [\" + _str(not_expected) + \"] +/- [\" + str(error_interval) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, not_expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)):\n\t\tif _is_almost_eq(got, not_expected, error_interval):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Helper function compares a value against a expected and a +/- range.  Compares\n# all components of Vector2 and Vector3 as well.\n# ------------------------------------------------------------------------------\nfunc _is_almost_eq(got, expected, error_interval) -> bool:\n\tvar result = false\n\tvar upper = expected + error_interval\n\tvar lower = expected - error_interval\n\n\tif typeof(got) == TYPE_VECTOR2:\n\t\tresult = got.x >= lower.x and got.x <= upper.x and \\\n\t\t\t\tgot.y >= lower.y and got.y <= upper.y\n\telif typeof(got) == TYPE_VECTOR3:\n\t\tresult = got.x >= lower.x and got.x <= upper.x and \\\n\t\t\t\tgot.y >= lower.y and got.y <= upper.y and \\\n\t\t\t\tgot.z >= lower.z and got.z <= upper.z\n\telse:\n\t\tresult = got >= (lower) and got <= (upper)\n\n\treturn(result)\n\n# ------------------------------------------------------------------------------\n# Asserts got is greater than expected\n# ------------------------------------------------------------------------------\nfunc assert_gt(got, expected, text=\"\"):\n\tvar disp = \"[\" + _str(got) + \"] expected to be > than [\" + _str(expected) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, expected, text)):\n\t\tif(got > expected):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts got is greater than or equal to expected\n# ------------------------------------------------------------------------------\nfunc assert_gte(got, expected, text=\"\"):\n\tvar disp = \"[\" + _str(got) + \"] expected to be >= than [\" + _str(expected) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, expected, text)):\n\t\tif(got >= expected):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts got is less than expected\n# ------------------------------------------------------------------------------\nfunc assert_lt(got, expected, text=\"\"):\n\tvar disp = \"[\" + _str(got) + \"] expected to be < than [\" + _str(expected) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, expected, text)):\n\t\tif(got < expected):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts got is less than or equal to expected\n# ------------------------------------------------------------------------------\nfunc assert_lte(got, expected, text=\"\"):\n\tvar disp = \"[\" + _str(got) + \"] expected to be <= than [\" + _str(expected) + \"]:  \" + text\n\tif(_do_datatypes_match__fail_if_not(got, expected, text)):\n\t\tif(got <= expected):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# asserts that got is true\n# ------------------------------------------------------------------------------\nfunc assert_true(got, text=\"\"):\n\tif(typeof(got) == TYPE_BOOL):\n\t\tif(got):\n\t\t\t_pass(text)\n\t\telse:\n\t\t\t_fail(text)\n\telse:\n\t\tvar msg = str(\"Cannot convert \", _strutils.type2str(got), \" to boolean\")\n\t\t_fail(msg)\n\n# ------------------------------------------------------------------------------\n# Asserts that got is false\n# ------------------------------------------------------------------------------\nfunc assert_false(got, text=\"\"):\n\tif(typeof(got) == TYPE_BOOL):\n\t\tif(got):\n\t\t\t_fail(text)\n\t\telse:\n\t\t\t_pass(text)\n\telse:\n\t\tvar msg = str(\"Cannot convert \", _strutils.type2str(got), \" to boolean\")\n\t\t_fail(msg)\n\n# ------------------------------------------------------------------------------\n# Asserts value is between (inclusive) the two expected values.\n# ------------------------------------------------------------------------------\nfunc assert_between(got, expect_low, expect_high, text=\"\"):\n\tvar disp = \"[\" + _str_precision(got, 20) + \"] expected to be between [\" + _str(expect_low) + \"] and [\" + str(expect_high) + \"]:  \" + text\n\n\tif(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)):\n\t\tif(expect_low > expect_high):\n\t\t\tdisp = \"INVALID range.  [\" + str(expect_low) + \"] is not less than [\" + str(expect_high) + \"]\"\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\tif(got < expect_low or got > expect_high):\n\t\t\t\t_fail(disp)\n\t\t\telse:\n\t\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts value is not between (exclusive) the two expected values.\n# ------------------------------------------------------------------------------\nfunc assert_not_between(got, expect_low, expect_high, text=\"\"):\n\tvar disp = \"[\" + _str_precision(got, 20) + \"] expected not to be between [\" + _str(expect_low) + \"] and [\" + str(expect_high) + \"]:  \" + text\n\n\tif(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)):\n\t\tif(expect_low > expect_high):\n\t\t\tdisp = \"INVALID range.  [\" + str(expect_low) + \"] is not less than [\" + str(expect_high) + \"]\"\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\tif(got > expect_low and got < expect_high):\n\t\t\t\t_fail(disp)\n\t\t\telse:\n\t\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Uses the 'has' method of the object passed in to determine if it contains\n# the passed in element.\n# ------------------------------------------------------------------------------\nfunc assert_has(obj, element, text=\"\"):\n\tvar disp = str('Expected [', _str(obj), '] to contain value:  [', _str(element), ']:  ', text)\n\tif(obj.has(element)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc assert_does_not_have(obj, element, text=\"\"):\n\tvar disp = str('Expected [', _str(obj), '] to NOT contain value:  [', _str(element), ']:  ', text)\n\tif(obj.has(element)):\n\t\t_fail(disp)\n\telse:\n\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that a file exists\n# ------------------------------------------------------------------------------\nfunc assert_file_exists(file_path):\n\tvar disp = 'expected [' + file_path + '] to exist.'\n\tif(FileAccess.file_exists(file_path)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that a file should not exist\n# ------------------------------------------------------------------------------\nfunc assert_file_does_not_exist(file_path):\n\tvar disp = 'expected [' + file_path + '] to NOT exist'\n\tif(!FileAccess.file_exists(file_path)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts the specified file is empty\n# ------------------------------------------------------------------------------\nfunc assert_file_empty(file_path):\n\tvar disp = 'expected [' + file_path + '] to be empty'\n\tif(FileAccess.file_exists(file_path) and gut.is_file_empty(file_path)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts the specified file is not empty\n# ------------------------------------------------------------------------------\nfunc assert_file_not_empty(file_path):\n\tvar disp = 'expected [' + file_path + '] to contain data'\n\tif(!gut.is_file_empty(file_path)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts the object has the specified method\n# ------------------------------------------------------------------------------\nfunc assert_has_method(obj, method, text=''):\n\tvar disp = _str(obj) + ' should have method: ' + method\n\tif(text != ''):\n\t\tdisp = _str(obj) + ' ' + text\n\tassert_true(obj.has_method(method), disp)\n\n\n# ------------------------------------------------------------------------------\n# Verifies the object has get and set methods for the property passed in.  The\n# property isn't tied to anything, just a name to be appended to the end of\n# get_ and set_.  Asserts the get_ and set_ methods exist, if not, it stops there.\n# If they exist then it asserts get_ returns the expected default then calls\n# set_ and asserts get_ has the value it was set to.\n# ------------------------------------------------------------------------------\nfunc assert_accessors(obj, property, default, set_to):\n\tvar fail_count = _summary.failed\n\tvar get_func = 'get_' + property\n\tvar set_func = 'set_' + property\n\n\tif(obj.has_method('is_' + property)):\n\t\tget_func = 'is_' + property\n\n\tassert_has_method(obj, get_func, 'should have getter starting with get_ or is_')\n\tassert_has_method(obj, set_func)\n\t# SHORT CIRCUIT\n\tif(_summary.failed > fail_count):\n\t\treturn\n\tassert_eq(obj.call(get_func), default, 'It should have the expected default value.')\n\tobj.call(set_func, set_to)\n\tassert_eq(obj.call(get_func), set_to, 'The set value should have been returned.')\n\n\n# ---------------------------------------------------------------------------\n# Property search helper.  Used to retrieve Dictionary of specified property\n# from passed object. Returns null if not found.\n# If provided, property_usage constrains the type of property returned by\n# passing either:\n# EDITOR_PROPERTY for properties defined as: export var some_value: int\n# VARIABLE_PROPERTY for properties defined as: var another_value\n# ---------------------------------------------------------------------------\nfunc _find_object_property(obj, property_name, property_usage=null):\n\tvar result = null\n\tvar found = false\n\tvar properties = obj.get_property_list()\n\n\twhile !found and !properties.is_empty():\n\t\tvar property = properties.pop_back()\n\t\tif property['name'] == property_name:\n\t\t\tif property_usage == null or property['usage'] == property_usage:\n\t\t\t\tresult = property\n\t\t\t\tfound = true\n\treturn result\n\n# ------------------------------------------------------------------------------\n# Asserts a class exports a variable.\n# ------------------------------------------------------------------------------\nfunc assert_exports(obj, property_name, type):\n\tvar disp = 'expected %s to have editor property [%s]' % [_str(obj), property_name]\n\tvar property = _find_object_property(obj, property_name, EDITOR_PROPERTY)\n\tif property != null:\n\t\tdisp += ' of type [%s]. Got type [%s].' % [_strutils.types[type], _strutils.types[property['type']]]\n\t\tif property['type'] == type:\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Signal assertion helper.\n#\n# Verifies that the object and signal are valid for making signal assertions.\n# This will fail with specific messages that indicate why they are not valid.\n# This returns true/false to indicate if the object and signal are valid.\n# ------------------------------------------------------------------------------\nfunc _can_make_signal_assertions(object, signal_name):\n\treturn !(_fail_if_not_watching(object) or _fail_if_does_not_have_signal(object, signal_name))\n\n# ------------------------------------------------------------------------------\n# Check if an object is connected to a signal on another object. Returns True\n# if it is and false otherwise\n# ------------------------------------------------------------------------------\nfunc _is_connected(signaler_obj, connect_to_obj, signal_name, method_name=\"\"):\n\tif(method_name != \"\"):\n\t\treturn signaler_obj.is_connected(signal_name,Callable(connect_to_obj,method_name))\n\telse:\n\t\tvar connections = signaler_obj.get_signal_connection_list(signal_name)\n\t\tfor conn in connections:\n\t\t\tif(conn['signal'].get_name() == signal_name and conn['callable'].get_object() == connect_to_obj):\n\t\t\t\treturn true\n\t\treturn false\n# ------------------------------------------------------------------------------\n# Watch the signals for an object.  This must be called before you can make\n# any assertions about the signals themselves.\n# ------------------------------------------------------------------------------\nfunc watch_signals(object):\n\t_signal_watcher.watch_signals(object)\n\n# ------------------------------------------------------------------------------\n# Asserts that an object is connected to a signal on another object\n#\n# This will fail with specific messages if the target object is not connected\n# to the specified signal on the source object.\n# ------------------------------------------------------------------------------\nfunc assert_connected(signaler_obj, connect_to_obj, signal_name, method_name=\"\"):\n\tpass\n\tvar method_disp = ''\n\tif (method_name != \"\"):\n\t\tmethod_disp = str(' using method: [', method_name, '] ')\n\tvar disp = str('Expected object ', _str(signaler_obj),\\\n\t\t' to be connected to signal: [', signal_name, '] on ',\\\n\t\t_str(connect_to_obj), method_disp)\n\tif(_is_connected(signaler_obj, connect_to_obj, signal_name, method_name)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that an object is not connected to a signal on another object\n#\n# This will fail with specific messages if the target object is connected\n# to the specified signal on the source object.\n# ------------------------------------------------------------------------------\nfunc assert_not_connected(signaler_obj, connect_to_obj, signal_name, method_name=\"\"):\n\tvar method_disp = ''\n\tif (method_name != \"\"):\n\t\tmethod_disp = str(' using method: [', method_name, '] ')\n\tvar disp = str('Expected object ', _str(signaler_obj),\\\n\t\t' to not be connected to signal: [', signal_name, '] on ',\\\n\t\t_str(connect_to_obj), method_disp)\n\tif(_is_connected(signaler_obj, connect_to_obj, signal_name, method_name)):\n\t\t_fail(disp)\n\telse:\n\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that a signal has been emitted at least once.\n#\n# This will fail with specific messages if the object is not being watched or\n# the object does not have the specified signal\n# ------------------------------------------------------------------------------\nfunc assert_signal_emitted(object, signal_name, text=\"\"):\n\tvar disp = str('Expected object ', _str(object), ' to have emitted signal [', signal_name, ']:  ', text)\n\tif(_can_make_signal_assertions(object, signal_name)):\n\t\tif(_signal_watcher.did_emit(object, signal_name)):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(_get_fail_msg_including_emitted_signals(disp, object))\n\n# ------------------------------------------------------------------------------\n# Asserts that a signal has not been emitted.\n#\n# This will fail with specific messages if the object is not being watched or\n# the object does not have the specified signal\n# ------------------------------------------------------------------------------\nfunc assert_signal_not_emitted(object, signal_name, text=\"\"):\n\tvar disp = str('Expected object ', _str(object), ' to NOT emit signal [', signal_name, ']:  ', text)\n\tif(_can_make_signal_assertions(object, signal_name)):\n\t\tif(_signal_watcher.did_emit(object, signal_name)):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that a signal was fired with the specified parameters.  The expected\n# parameters should be passed in as an array.  An optional index can be passed\n# when a signal has fired more than once.  The default is to retrieve the most\n# recent emission of the signal.\n#\n# This will fail with specific messages if the object is not being watched or\n# the object does not have the specified signal\n# ------------------------------------------------------------------------------\nfunc assert_signal_emitted_with_parameters(object, signal_name, parameters, index=-1):\n\tif(typeof(parameters) != TYPE_ARRAY):\n\t\t_lgr.error(\"The expected parameters must be wrapped in an array, you passed:  \" + _str(parameters))\n\t\t_fail(\"Bad Parameters\")\n\t\treturn\n\n\tvar disp = str('Expected object ', _str(object), ' to emit signal [', signal_name, '] with parameters ', parameters, ', got ')\n\tif(_can_make_signal_assertions(object, signal_name)):\n\t\tif(_signal_watcher.did_emit(object, signal_name)):\n\t\t\tvar parms_got = _signal_watcher.get_signal_parameters(object, signal_name, index)\n\t\t\tvar diff_result = _compare.deep(parameters, parms_got)\n\t\t\tif(diff_result.are_equal):\n\t\t\t\t_pass(str(disp, parms_got))\n\t\t\telse:\n\t\t\t\t_fail(str('Expected object ', _str(object), ' to emit signal [', signal_name, '] with parameters ', diff_result.summarize()))\n\t\telse:\n\t\t\tvar text = str('Object ', object, ' did not emit signal [', signal_name, ']')\n\t\t\t_fail(_get_fail_msg_including_emitted_signals(text, object))\n\n# ------------------------------------------------------------------------------\n# Assert that a signal has been emitted a specific number of times.\n#\n# This will fail with specific messages if the object is not being watched or\n# the object does not have the specified signal\n# ------------------------------------------------------------------------------\nfunc assert_signal_emit_count(object, signal_name, times, text=\"\"):\n\tif(_can_make_signal_assertions(object, signal_name)):\n\t\tvar count = _signal_watcher.get_emit_count(object, signal_name)\n\t\tvar disp = str('Expected the signal [', signal_name, '] emit count of [', count, '] to equal [', times, ']: ', text)\n\t\tif(count== times):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(_get_fail_msg_including_emitted_signals(disp, object))\n\n# ------------------------------------------------------------------------------\n# Assert that the passed in object has the specified signal\n# ------------------------------------------------------------------------------\nfunc assert_has_signal(object, signal_name, text=\"\"):\n\tvar disp = str('Expected object ', _str(object), ' to have signal [', signal_name, ']:  ', text)\n\tif(_signal_watcher.does_object_have_signal(object, signal_name)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n\n# ------------------------------------------------------------------------------\n# Returns the number of times a signal was emitted.  -1 returned if the object\n# is not being watched.\n# ------------------------------------------------------------------------------\nfunc get_signal_emit_count(object, signal_name):\n\treturn _signal_watcher.get_emit_count(object, signal_name)\n\n# ------------------------------------------------------------------------------\n# Get the parmaters of a fired signal.  If the signal was not fired null is\n# returned.  You can specify an optional index (use get_signal_emit_count to\n# determine the number of times it was emitted).  The default index is the\n# latest time the signal was fired (size() -1 insetead of 0).  The parameters\n# returned are in an array.\n# ------------------------------------------------------------------------------\nfunc get_signal_parameters(object, signal_name, index=-1):\n\treturn _signal_watcher.get_signal_parameters(object, signal_name, index)\n\n# ------------------------------------------------------------------------------\n# Get the parameters for a method call to a doubled object.  By default it will\n# return the most recent call.  You can optionally specify an index.\n#\n# Returns:\n# * an array of parameter values if a call the method was found\n# * null when a call to the method was not found or the index specified was\n#   invalid.\n# ------------------------------------------------------------------------------\nfunc get_call_parameters(object, method_name, index=-1):\n\tvar to_return = null\n\tif(GutUtils.is_double(object)):\n\t\tto_return = gut.get_spy().get_call_parameters(object, method_name, index)\n\telse:\n\t\t_lgr.error('You must pass a doulbed object to get_call_parameters.')\n\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# Returns the call count for a method with optional paramter matching.\n# ------------------------------------------------------------------------------\nfunc get_call_count(object, method_name, parameters=null):\n\treturn gut.get_spy().call_count(object, method_name, parameters)\n\n\n# ------------------------------------------------------------------------------\n# Assert that object is an instance of a_class\n# ------------------------------------------------------------------------------\nfunc assert_is(object, a_class, text=''):\n\tvar disp  = ''#var disp = str('Expected [', _str(object), '] to be type of [', a_class, ']: ', text)\n\tvar NATIVE_CLASS = 'GDScriptNativeClass'\n\tvar GDSCRIPT_CLASS = 'GDScript'\n\tvar bad_param_2 = 'Parameter 2 must be a Class (like Node2D or Label).  You passed '\n\n\tif(typeof(object) != TYPE_OBJECT):\n\t\t_fail(str('Parameter 1 must be an instance of an object.  You passed:  ', _str(object)))\n\telif(typeof(a_class) != TYPE_OBJECT):\n\t\t_fail(str(bad_param_2, _str(a_class)))\n\telse:\n\t\tvar a_str = _str(a_class)\n\t\tdisp = str('Expected [', _str(object), '] to extend [', a_str, ']: ', text)\n\t\tif(!GutUtils.is_native_class(a_class) and !GutUtils.is_gdscript(a_class)):\n\t\t\t_fail(str(bad_param_2, a_str))\n\t\telse:\n\t\t\tif(is_instance_of(object, a_class)):\n\t\t\t\t_pass(disp)\n\t\t\telse:\n\t\t\t\t_fail(disp)\n\nfunc _get_typeof_string(the_type):\n\tvar to_return = \"\"\n\tif(_strutils.types.has(the_type)):\n\t\tto_return += str(the_type, '(',  _strutils.types[the_type], ')')\n\telse:\n\t\tto_return += str(the_type)\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc assert_typeof(object, type, text=''):\n\tvar disp = str('Expected [typeof(', object, ') = ')\n\tdisp += _get_typeof_string(typeof(object))\n\tdisp += '] to equal ['\n\tdisp += _get_typeof_string(type) +  ']'\n\tdisp += '.  ' + text\n\tif(typeof(object) == type):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc assert_not_typeof(object, type, text=''):\n\tvar disp = str('Expected [typeof(', object, ') = ')\n\tdisp += _get_typeof_string(typeof(object))\n\tdisp += '] to not equal ['\n\tdisp += _get_typeof_string(type) +  ']'\n\tdisp += '.  ' + text\n\tif(typeof(object) != type):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Assert that text contains given search string.\n# The match_case flag determines case sensitivity.\n# ------------------------------------------------------------------------------\nfunc assert_string_contains(text, search, match_case=true):\n\tconst empty_search = 'Expected text and search strings to be non-empty. You passed %s and %s.'\n\tconst non_strings = 'Expected text and search to both be strings.  You passed %s and %s.'\n\tvar disp = 'Expected \\'%s\\' to contain \\'%s\\', match_case=%s' % [text, search, match_case]\n\tif(typeof(text) != TYPE_STRING or typeof(search) != TYPE_STRING):\n\t\t_fail(non_strings % [_str(text), _str(search)])\n\telif(text == '' or search == ''):\n\t\t_fail(empty_search % [_str(text), _str(search)])\n\telif(match_case):\n\t\tif(text.find(search) == -1):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\telse:\n\t\tif(text.to_lower().find(search.to_lower()) == -1):\n\t\t\t_fail(disp)\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Assert that text starts with given search string.\n# match_case flag determines case sensitivity.\n# ------------------------------------------------------------------------------\nfunc assert_string_starts_with(text, search, match_case=true):\n\tvar empty_search = 'Expected text and search strings to be non-empty. You passed \\'%s\\' and \\'%s\\'.'\n\tvar disp = 'Expected \\'%s\\' to start with \\'%s\\', match_case=%s' % [text, search, match_case]\n\tif(text == '' or search == ''):\n\t\t_fail(empty_search % [text, search])\n\telif(match_case):\n\t\tif(text.find(search) == 0):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\telse:\n\t\tif(text.to_lower().find(search.to_lower()) == 0):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n\n# ------------------------------------------------------------------------------\n# Assert that text ends with given search string.\n# match_case flag determines case sensitivity.\n# ------------------------------------------------------------------------------\nfunc assert_string_ends_with(text, search, match_case=true):\n\tvar empty_search = 'Expected text and search strings to be non-empty. You passed \\'%s\\' and \\'%s\\'.'\n\tvar disp = 'Expected \\'%s\\' to end with \\'%s\\', match_case=%s' % [text, search, match_case]\n\tvar required_index = len(text) - len(search)\n\tif(text == '' or search == ''):\n\t\t_fail(empty_search % [text, search])\n\telif(match_case):\n\t\tif(text.find(search) == required_index):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\telse:\n\t\tif(text.to_lower().find(search.to_lower()) == required_index):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(disp)\n\n\n# ------------------------------------------------------------------------------\n# Assert that a method was called on an instance of a doubled class.  If\n# parameters are supplied then the params passed in when called must match.\n# TODO make 3rd parameter \"param_or_text\" and add fourth parameter of \"text\" and\n#      then work some magic so this can have a \"text\" parameter without being\n#      annoying.\n# ------------------------------------------------------------------------------\nfunc assert_called(inst, method_name, parameters=null):\n\tvar disp = str('Expected [',method_name,'] to have been called on ',_str(inst))\n\n\tif(_fail_if_parameters_not_array(parameters)):\n\t\treturn\n\n\tif(_fail_if_not_double_or_does_not_have_method(inst, method_name) == OK):\n\t\tif(gut.get_spy().was_called(inst, method_name, parameters)):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\tif(parameters != null):\n\t\t\t\tdisp += str(' with parameters ', parameters)\n\t\t\t_fail(str(disp, \"\\n\", _get_desc_of_calls_to_instance(inst)))\n\n# ------------------------------------------------------------------------------\n# Assert that a method was not called on an instance of a doubled class.  If\n# parameters are specified then this will only fail if it finds a call that was\n# sent matching parameters.\n# ------------------------------------------------------------------------------\nfunc assert_not_called(inst, method_name, parameters=null):\n\tvar disp = str('Expected [', method_name, '] to NOT have been called on ', _str(inst))\n\n\tif(_fail_if_parameters_not_array(parameters)):\n\t\treturn\n\n\tif(_fail_if_not_double_or_does_not_have_method(inst, method_name) == OK):\n\t\tif(gut.get_spy().was_called(inst, method_name, parameters)):\n\t\t\tif(parameters != null):\n\t\t\t\tdisp += str(' with parameters ', parameters)\n\t\t\t_fail(str(disp, \"\\n\", _get_desc_of_calls_to_instance(inst)))\n\t\telse:\n\t\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Assert that a method on an instance of a doubled class was called a number\n# of times.  If parameters are specified then only calls with matching\n# parameter values will be counted.\n# ------------------------------------------------------------------------------\nfunc assert_call_count(inst, method_name, expected_count, parameters=null):\n\tvar count = gut.get_spy().call_count(inst, method_name, parameters)\n\n\tif(_fail_if_parameters_not_array(parameters)):\n\t\treturn\n\n\tvar param_text = ''\n\tif(parameters):\n\t\tparam_text = ' with parameters ' + str(parameters)\n\tvar disp = 'Expected [%s] on %s to be called [%s] times%s.  It was called [%s] times.'\n\tdisp = disp % [method_name, _str(inst), expected_count, param_text, count]\n\n\tif(_fail_if_not_double_or_does_not_have_method(inst, method_name) == OK):\n\t\tif(count == expected_count):\n\t\t\t_pass(disp)\n\t\telse:\n\t\t\t_fail(str(disp, \"\\n\", _get_desc_of_calls_to_instance(inst)))\n\n# ------------------------------------------------------------------------------\n# Asserts the passed in value is null\n# ------------------------------------------------------------------------------\nfunc assert_null(got, text=''):\n\tvar disp = str('Expected [', _str(got), '] to be NULL:  ', text)\n\tif(got == null):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\n# ------------------------------------------------------------------------------\n# Asserts the passed in value is null\n# ------------------------------------------------------------------------------\nfunc assert_not_null(got, text=''):\n\tvar disp = str('Expected [', _str(got), '] to be anything but NULL:  ', text)\n\tif(got == null):\n\t\t_fail(disp)\n\telse:\n\t\t_pass(disp)\n\n# -----------------------------------------------------------------------------\n# Asserts object has been freed from memory\n# We pass in a title (since if it is freed, we lost all identity data)\n# -----------------------------------------------------------------------------\nfunc assert_freed(obj, title='something'):\n\tvar disp = title\n\tif(is_instance_valid(obj)):\n\t\tdisp = _strutils.type2str(obj) + title\n\tassert_true(not is_instance_valid(obj), \"Expected [%s] to be freed\" % disp)\n\n# ------------------------------------------------------------------------------\n# Asserts Object has not been freed from memory\n# -----------------------------------------------------------------------------\nfunc assert_not_freed(obj, title):\n\tvar disp = title\n\tif(is_instance_valid(obj)):\n\t\tdisp = _strutils.type2str(obj) + title\n\tassert_true(is_instance_valid(obj), \"Expected [%s] to not be freed\" % disp)\n\n# ------------------------------------------------------------------------------\n# Asserts that the current test has not introduced any new orphans.  This only\n# applies to the test code that preceedes a call to this method so it should be\n# the last thing your test does.\n# ------------------------------------------------------------------------------\nfunc assert_no_new_orphans(text=''):\n\tvar count = gut.get_orphan_counter().get_orphans_since('test')\n\tvar msg = ''\n\tif(text != ''):\n\t\tmsg = ':  ' + text\n\t# Note that get_counter will return -1 if the counter does not exist.  This\n\t# can happen with a misplaced assert_no_new_orphans.  Checking for > 0\n\t# ensures this will not cause some weird failure.\n\tif(count > 0):\n\t\t_fail(str('Expected no orphans, but found ', count, msg))\n\telse:\n\t\t_pass('No new orphans found.' + msg)\n\n\n# ------------------------------------------------------------------------------\n# Validates the singleton_name is a string and exists.  Errors when conditions\n# are not met.  Returns true/false if singleton_name is valid or not.\n# ------------------------------------------------------------------------------\nfunc _validate_singleton_name(singleton_name):\n\tvar is_valid = true\n\tif(typeof(singleton_name) != TYPE_STRING):\n\t\t_lgr.error(\"double_singleton requires a Godot singleton name, you passed \" + _str(singleton_name))\n\t\tis_valid = false\n\t# Sometimes they have underscores in front of them, sometimes they do not.\n\t# The doubler is smart enought of ind the right thing, so this has to be\n\t# that smart as well.\n\telif(!ClassDB.class_exists(singleton_name) and !ClassDB.class_exists('_' + singleton_name)):\n\t\tvar txt = str(\"The singleton [\", singleton_name, \"] could not be found.  \",\n\t\t\t\t\t\"Check the GlobalScope page for a list of singletons.\")\n\t\t_lgr.error(txt)\n\t\tis_valid = false\n\treturn is_valid\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc assert_setget(\n\tinstance, name_property,\n\tconst_or_setter = null, getter=\"__not_set__\"):\n\t_lgr.deprecated('assert_setget')\n\t_fail('assert_setget has been removed.  Use assert_property, assert_set_property, assert_readonly_property instead.')\n\n\n# ------------------------------------------------------------------------------\n# This will set the property through the setter and compare the result to the\n# expected value.  Useful when setter is not simple.\n# ------------------------------------------------------------------------------\nfunc assert_set_property(obj, property_name, new_value, expected_value):\n\tpending(\"this hasn't been implemented yet\")\n\n\n# ------------------------------------------------------------------------------\n# This will attempt to assign new_value to the property and verify that it\n# is equal to expected_value.\n# ------------------------------------------------------------------------------\nfunc assert_readonly_property(obj, property_name, new_value, expected_value):\n\tpending(\"this hasn't been implemented yet\")\n\n\n# ------------------------------------------------------------------------------\n# Checks the object for 'get_' and 'set_' methods for the specified property.\n# If found a warning is generated.\n# ------------------------------------------------------------------------------\nfunc _warn_for_public_accessors(obj, property_name):\n\tvar public_accessors = []\n\tvar accessor_names = [\n\t\tstr('get_', property_name),\n\t\tstr('is_', property_name),\n\t\tstr('set_', property_name)\n\t]\n\n\tfor acc in accessor_names:\n\t\tif(obj.has_method(acc)):\n\t\t\tpublic_accessors.append(acc)\n\n\tif(public_accessors.size() > 0):\n\t\t_lgr.warn (str('Public accessors ', public_accessors, ' found for property ', property_name))\n\n# ------------------------------------------------------------------------------\n# Assumes backing varible with be _<property_name>.  This will perform all the\n# asserts of assert_property.  Then this will set the value through the setter\n# and check the backing variable value.  It will then reset throught the setter\n# and set the backing variable and check the getter.\n# ------------------------------------------------------------------------------\nfunc assert_property_with_backing_variable(obj, property_name, default_value, new_value, backed_by_name=null):\n\tvar setter_name = str('@', property_name, '_setter')\n\tvar getter_name = str('@', property_name, '_getter')\n\tvar backing_name = GutUtils.nvl(backed_by_name, str('_', property_name))\n\tvar pre_fail_count = get_fail_count()\n\n\tvar props = obj.get_property_list()\n\tvar found = false\n\tvar idx = 0\n\twhile(idx < props.size() and !found):\n\t\tfound = props[idx].name == backing_name\n\t\tidx += 1\n\n\tassert_true(found, str(obj, ' has ', backing_name, ' variable.'))\n\tassert_true(obj.has_method(setter_name), str('There should be a setter for ', property_name))\n\tassert_true(obj.has_method(getter_name), str('There should be a getter for ', property_name))\n\n\tif(pre_fail_count == get_fail_count()):\n\t\tvar call_setter = Callable(obj, setter_name)\n\t\tvar call_getter = Callable(obj, getter_name)\n\n\t\tassert_eq(obj.get(backing_name), default_value, str('Variable ', backing_name, ' has default value.'))\n\t\tassert_eq(call_getter.call(), default_value, 'Getter returns default value.')\n\t\tcall_setter.call(new_value)\n\t\tassert_eq(call_getter.call(), new_value, 'Getter returns value from Setter.')\n\t\tassert_eq(obj.get(backing_name), new_value, str('Variable ', backing_name, ' was set'))\n\n\t_warn_for_public_accessors(obj, property_name)\n\n\n# ------------------------------------------------------------------------------\n# This will verify that the method has a setter and getter for the property.\n# It will then use the getter to check the default.  Then use the\n# setter with new_value and verify the getter returns the same value.\n# ------------------------------------------------------------------------------\nfunc assert_property(obj, property_name, default_value, new_value) -> void:\n\tvar free_me = null\n\tvar resource = null\n\tvar pre_fail_count = get_fail_count()\n\n\tvar setter_name = str('@', property_name, '_setter')\n\tvar getter_name = str('@', property_name, '_getter')\n\n\tif(typeof(obj) != TYPE_OBJECT):\n\t\t_fail(str(_str(obj), ' is not an object'))\n\t\treturn\n\n\tassert_has_method(obj, setter_name)\n\tassert_has_method(obj, getter_name)\n\n\tif(pre_fail_count == get_fail_count()):\n\t\tvar call_setter = Callable(obj, setter_name)\n\t\tvar call_getter = Callable(obj, getter_name)\n\n\t\tassert_eq(call_getter.call(), default_value, 'Default value')\n\t\tcall_setter.call(new_value)\n\t\tassert_eq(call_getter.call(), new_value, 'Getter gets Setter value')\n\n\t_warn_for_public_accessors(obj, property_name)\n\n# ------------------------------------------------------------------------------\n# Mark the current test as pending.\n# ------------------------------------------------------------------------------\nfunc pending(text=\"\"):\n\t_summary.pending += 1\n\tif(gut):\n\t\t_lgr.pending(text)\n\t\tgut._pending(text)\n\n\n# ------------------------------------------------------------------------------\n# Yield for the time sent in.  The optional message will be printed when\n# Gut detects the yield.\n# ------------------------------------------------------------------------------\nfunc wait_seconds(time, msg=''):\n\t_lgr.yield_msg(str('-- Awaiting ', time, ' second(s) -- ', msg))\n\t_awaiter.wait_seconds(time)\n\treturn _awaiter.timeout\n\nfunc yield_for(time, msg=''):\n\t_lgr.deprecated('yield_for', 'wait_seconds')\n\treturn wait_seconds(time, msg)\n\n\n# ------------------------------------------------------------------------------\n# Yield to a signal or a maximum amount of time, whichever comes first.\n# ------------------------------------------------------------------------------\nfunc wait_for_signal(sig : Signal, max_wait, msg=''):\n\twatch_signals(sig.get_object())\n\t_lgr.yield_msg(str('-- Awaiting signal \"', sig.get_name(), '\" or for ', max_wait, ' second(s) -- ', msg))\n\t_awaiter.wait_for_signal(sig, max_wait)\n\tawait _awaiter.timeout\n\treturn !_awaiter.did_last_wait_timeout\n\n\nfunc yield_to(obj, signal_name, max_wait, msg=''):\n\t_lgr.deprecated('yield_to', 'wait_for_signal')\n\treturn await wait_for_signal(Signal(obj, signal_name), max_wait, msg)\n\n\n# ------------------------------------------------------------------------------\n# Yield for a number of frames.  The optional message will be printed. when\n# Gut detects a yield.\n# ------------------------------------------------------------------------------\nfunc wait_frames(frames, msg=''):\n\tif(frames <= 0):\n\t\tvar text = str('yeild_frames:  frames must be > 0, you passed  ', frames, '.  0 frames waited.')\n\t\t_lgr.error(text)\n\t\tframes = 1\n\n\t_lgr.yield_msg(str('-- Awaiting ', frames, ' frame(s) -- ', msg))\n\t_awaiter.wait_frames(frames)\n\treturn _awaiter.timeout\n\n# p3 can be the optional message or an amount of time to wait between tests.\n# p4 is the optional message if you have specified an amount of time to\n#\twait between tests.\nfunc wait_until(callable, max_wait, p3='', p4=''):\n\tvar time_between = 0.0\n\tvar message = p4\n\tif(typeof(p3) != TYPE_STRING):\n\t\ttime_between = p3\n\telse:\n\t\tmessage = p3\n\n\t_lgr.yield_msg(str(\"--Awaiting callable to return TRUE or \", max_wait, \"s.  \", message))\n\t_awaiter.wait_until(callable, max_wait, time_between)\n\tawait _awaiter.timeout\n\treturn !_awaiter.did_last_wait_timeout\n\n\nfunc did_wait_timeout():\n\treturn _awaiter.did_last_wait_timeout\n\n\nfunc yield_frames(frames, msg=''):\n\t_lgr.deprecated(\"yield_frames\", \"wait_frames\")\n\treturn wait_frames(frames, msg)\n\n\nfunc get_summary():\n\treturn _summary\n\nfunc get_fail_count():\n\treturn _summary.failed\n\nfunc get_pass_count():\n\treturn _summary.passed\n\nfunc get_pending_count():\n\treturn _summary.pending\n\nfunc get_assert_count():\n\treturn _summary.asserts\n\nfunc clear_signal_watcher():\n\t_signal_watcher.clear()\n\nfunc get_double_strategy():\n\treturn gut.get_doubler().get_strategy()\n\nfunc set_double_strategy(double_strategy):\n\tgut.get_doubler().set_strategy(double_strategy)\n\nfunc pause_before_teardown():\n\tgut.pause_before_teardown()\n\n# ------------------------------------------------------------------------------\n# Convert the _summary dictionary into text\n# ------------------------------------------------------------------------------\nfunc get_summary_text():\n\tvar to_return = get_script().get_path() + \"\\n\"\n\tto_return += str('  ', _summary.passed, ' of ', _summary.asserts, ' passed.')\n\tif(_summary.pending > 0):\n\t\tto_return += str(\"\\n  \", _summary.pending, ' pending')\n\tif(_summary.failed > 0):\n\t\tto_return += str(\"\\n  \", _summary.failed, ' failed.')\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# Double a script, inner class, or scene using a path or a loaded script/scene.\n#\n#\n# ------------------------------------------------------------------------------\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc _smart_double(thing, double_strat, partial):\n\tvar override_strat = GutUtils.nvl(double_strat, gut.get_doubler().get_strategy())\n\tvar to_return = null\n\n\tif(thing is PackedScene):\n\t\tif(partial):\n\t\t\tto_return =  gut.get_doubler().partial_double_scene(thing, override_strat)\n\t\telse:\n\t\t\tto_return =  gut.get_doubler().double_scene(thing, override_strat)\n\n\telif(GutUtils.is_native_class(thing)):\n\t\tif(partial):\n\t\t\tto_return = gut.get_doubler().partial_double_gdnative(thing)\n\t\telse:\n\t\t\tto_return = gut.get_doubler().double_gdnative(thing)\n\n\telif(thing is GDScript):\n\t\tif(partial):\n\t\t\tto_return = gut.get_doubler().partial_double(thing, override_strat)\n\t\telse:\n\t\t\tto_return = gut.get_doubler().double(thing, override_strat)\n\n\treturn to_return\n\n# ------------------------------------------------------------------------------\n# This is here to aid in the transition to the new doubling sytnax.  Once this\n# has been established it could be removed.  We must keep the is_instance check\n# going forward though.\n# ------------------------------------------------------------------------------\nfunc _are_double_parameters_valid(thing, p2, p3):\n\tvar bad_msg = \"\"\n\tif(p3 != null or typeof(p2) == TYPE_STRING):\n\t\tbad_msg += \"Doubling using a subpath is not supported.  Call register_inner_class and then pass the Inner Class to double().\\n\"\n\n\tif(typeof(thing) == TYPE_STRING):\n\t\tbad_msg += \"Doubling using the path to a script or scene is no longer supported.  Load the script or scene and pass that to double instead.\\n\"\n\n\tif(GutUtils.is_instance(thing)):\n\t\tbad_msg += \"double requires a script, you passed an instance:  \" + _str(thing)\n\n\tif(bad_msg != \"\"):\n\t\t_lgr.error(bad_msg)\n\n\treturn bad_msg == \"\"\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc double(thing, double_strat=null, not_used_anymore=null):\n\tif(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)):\n\t\treturn null\n\n\treturn _smart_double(thing, double_strat, false)\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nfunc partial_double(thing, double_strat=null, not_used_anymore=null):\n\tif(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)):\n\t\treturn null\n\n\treturn _smart_double(thing, double_strat, true)\n\n# ------------------------------------------------------------------------------\n# Doubles a Godot singleton\n# ------------------------------------------------------------------------------\nfunc double_singleton(singleton_name):\n\treturn null\n\t# var to_return = null\n\t# if(_validate_singleton_name(singleton_name)):\n\t# \tto_return = gut.get_doubler().double_singleton(singleton_name)\n\t# return to_return\n\n# ------------------------------------------------------------------------------\n# Partial Doubles a Godot singleton\n# ------------------------------------------------------------------------------\nfunc partial_double_singleton(singleton_name):\n\treturn null\n\t# var to_return = null\n\t# if(_validate_singleton_name(singleton_name)):\n\t# \tto_return = gut.get_doubler().partial_double_singleton(singleton_name)\n\t# return to_return\n\n# ------------------------------------------------------------------------------\n# Specifically double a scene\n# ------------------------------------------------------------------------------\nfunc double_scene(path, strategy=null):\n\t_lgr.deprecated('test.double_scene has been removed.', 'double')\n\treturn null\n\n\t# var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())\n\t# return gut.get_doubler().double_scene(path, override_strat)\n\n# ------------------------------------------------------------------------------\n# Specifically double a script\n# ------------------------------------------------------------------------------\nfunc double_script(path, strategy=null):\n\t_lgr.deprecated('test.double_script has been removed.', 'double')\n\treturn null\n\n\t# var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())\n\t# return gut.get_doubler().double(path, override_strat)\n\n# ------------------------------------------------------------------------------\n# Specifically double an Inner class in a a script\n# ------------------------------------------------------------------------------\nfunc double_inner(path, subpath, strategy=null):\n\t_lgr.deprecated('double_inner should not be used.  Use register_inner_classes and double instead.', 'double')\n\treturn null\n\n\tvar override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())\n\treturn gut.get_doubler().double_inner(path, subpath, override_strat)\n\n\n# ------------------------------------------------------------------------------\n# Add a method that the doubler will ignore.  You can pass this a loaded script\n# or scene.  These ignores are cleared after every test.\n# ------------------------------------------------------------------------------\nfunc ignore_method_when_doubling(thing, method_name):\n\tif(typeof(thing) == TYPE_STRING):\n\t\t_lgr.error('ignore_method_when_doubling no longer supports paths to scripts or scenes.  Load them and pass them instead.')\n\t\treturn\n\n\tvar r = thing\n\tif(thing is PackedScene):\n\t\tr = GutUtils.get_scene_script_object(thing)\n\n\tgut.get_doubler().add_ignored_method(r, method_name)\n\n# ------------------------------------------------------------------------------\n# Stub something.\n#\n# Parameters\n# 1: A callable OR the thing to stub OR a file path OR an instance OR a Script\n# 2: either an inner class subpath or the method name\n# 3: the method name if an inner class subpath was specified\n# NOTE:  right now we cannot stub inner classes at the path level so this should\n#        only be called with two parameters.  I did the work though so I'm going\n#        to leave it but not update the wiki.\n# ------------------------------------------------------------------------------\nfunc stub(thing, p2=null, p3=null):\n\tvar method_name = p2\n\tvar subpath = null\n\n\tif(p3 != null):\n\t\tsubpath = p2\n\t\tmethod_name = p3\n\n\tif(GutUtils.is_instance(thing)):\n\t\tvar msg = _get_bad_double_or_method_message(thing, method_name, 'stub')\n\t\tif(msg != ''):\n\t\t\t_lgr.error(msg)\n\t\t\treturn GutUtils.StubParams.new()\n\n\tvar sp = null\n\tif(typeof(thing) == TYPE_CALLABLE):\n\t\tif(p2 != null or p3 != null):\n\t\t\t_lgr.error(\"Only one parameter expected when using a callable.\")\n\t\tsp = GutUtils.StubParams.new(thing)\n\telse:\n\t\tsp = GutUtils.StubParams.new(thing, method_name, subpath)\n\n\tsp.logger = _lgr\n\tgut.get_stubber().add_stub(sp)\n\treturn sp\n\n# ------------------------------------------------------------------------------\n# convenience wrapper.\n# ------------------------------------------------------------------------------\nfunc simulate(obj, times, delta, check_is_processing: bool = false):\n\tgut.simulate(obj, times, delta, check_is_processing)\n\n# ------------------------------------------------------------------------------\n# Replace the node at base_node.get_node(path) with with_this.  All references\n# to the node via $ and get_node(...) will now return with_this.  with_this will\n# get all the groups that the node that was replaced had.\n#\n# The node that was replaced is queued to be freed.\n#\n# TODO see replace_by method, this could simplify the logic here.\n# ------------------------------------------------------------------------------\nfunc replace_node(base_node, path_or_node, with_this):\n\tvar path = path_or_node\n\n\tif(typeof(path_or_node) != TYPE_STRING):\n\t\t# This will cause an engine error if it fails.  It always returns a\n\t\t# NodePath, even if it fails.  Checking the name count is the only way\n\t\t# I found to check if it found something or not (after it worked I\n\t\t# didn't look any farther).\n\t\tpath = base_node.get_path_to(path_or_node)\n\t\tif(path.get_name_count() == 0):\n\t\t\t_lgr.error('You passed an object that base_node does not have.  Cannot replace node.')\n\t\t\treturn\n\n\tif(!base_node.has_node(path)):\n\t\t_lgr.error(str('Could not find node at path [', path, ']'))\n\t\treturn\n\n\tvar to_replace = base_node.get_node(path)\n\tvar parent = to_replace.get_parent()\n\tvar replace_name = to_replace.get_name()\n\n\tparent.remove_child(to_replace)\n\tparent.add_child(with_this)\n\twith_this.set_name(replace_name)\n\twith_this.set_owner(parent)\n\n\tvar groups = to_replace.get_groups()\n\tfor i in range(groups.size()):\n\t\twith_this.add_to_group(groups[i])\n\n\tto_replace.queue_free()\n\n\n# ------------------------------------------------------------------------------\n# This method does a somewhat complicated dance with Gut.  It assumes that Gut\n# will clear its parameter handler after it finishes calling a parameterized test\n# enough times.\n# ------------------------------------------------------------------------------\nfunc use_parameters(params):\n\tvar ph = gut.parameter_handler\n\tif(ph == null):\n\t\tph = GutUtils.ParameterHandler.new(params)\n\t\tgut.parameter_handler = ph\n\n\t# DO NOT use gut.gd's get_call_count_text here since it decrements the\n\t# get_call_count value.  This method increments the call count in its\n\t# return statement.\n\tvar output = str('- params[', ph.get_call_count(), ']','(', ph.get_current_parameters(), ')')\n\tgut.p(output, gut.LOG_LEVEL_TEST_AND_FAILURES)\n\n\treturn ph.next_parameters()\n\n\n# ------------------------------------------------------------------------------\n# When used as the default for a test method parameter, it will cause the test\n# to be run x times.\n#\n# I Hacked this together to test a method that was occassionally failing due to\n# timing issues.  I don't think it's a great idea, but you be the judge.\n# ------------------------------------------------------------------------------\nfunc run_x_times(x):\n\tvar ph = gut.parameter_handler\n\tif(ph == null):\n\t\t_lgr.warn(\n\t\t\tstr(\"This test uses run_x_times and you really should not be \",\n\t\t\t\"using it.  I don't think it's a good thing, but I did find it \",\n\t\t\t\"temporarily useful so I left it in here and didn't document it.  \",\n\t\t\t\"Well, you found it, might as well open up an issue and let me \",\n\t\t\t\"know why you're doing this.\"))\n\t\tvar params = []\n\t\tfor i in range(x):\n\t\t\tparams.append(i)\n\n\t\tph = GutUtils.ParameterHandler.new(params)\n\t\tgut.parameter_handler = ph\n\treturn ph.next_parameters()\n\n# ------------------------------------------------------------------------------\n# Marks whatever is passed in to be freed after the test finishes.  It also\n# returns what is passed in so you can save a line of code.\n#   var thing = autofree(Thing.new())\n# ------------------------------------------------------------------------------\nfunc autofree(thing):\n\tgut.get_autofree().add_free(thing)\n\treturn thing\n\n# ------------------------------------------------------------------------------\n# Works the same as autofree except queue_free will be called on the object\n# instead.  This also imparts a brief pause after the test finishes so that\n# the queued object has time to free.\n# ------------------------------------------------------------------------------\nfunc autoqfree(thing):\n\tgut.get_autofree().add_queue_free(thing)\n\treturn thing\n\n# ------------------------------------------------------------------------------\n# The same as autofree but it also adds the object as a child of the test.\n# ------------------------------------------------------------------------------\nfunc add_child_autofree(node, legible_unique_name = false):\n\tgut.get_autofree().add_free(node)\n\t# Explicitly calling super here b/c add_child MIGHT change and I don't want\n\t# a bug sneaking its way in here.\n\tsuper.add_child(node, legible_unique_name)\n\treturn node\n\n# ------------------------------------------------------------------------------\n# The same as autoqfree but it also adds the object as a child of the test.\n# ------------------------------------------------------------------------------\nfunc add_child_autoqfree(node, legible_unique_name=false):\n\tgut.get_autofree().add_queue_free(node)\n\t# Explicitly calling super here b/c add_child MIGHT change and I don't want\n\t# a bug sneaking its way in here.\n\tsuper.add_child(node, legible_unique_name)\n\treturn node\n\n# ------------------------------------------------------------------------------\n# Returns true if the test is passing as of the time of this call.  False if not.\n# ------------------------------------------------------------------------------\nfunc is_passing():\n\tif(gut.get_current_test_object() != null and\n\t\t!['before_all', 'after_all'].has(gut.get_current_test_object().name)):\n\t\treturn gut.get_current_test_object().is_passing() and \\\n\t\t\tgut.get_current_test_object().assert_count > 0\n\telse:\n\t\t_lgr.error('No current test object found.  is_passing must be called inside a test.')\n\t\treturn null\n\n# ------------------------------------------------------------------------------\n# Returns true if the test is failing as of the time of this call.  False if not.\n# ------------------------------------------------------------------------------\nfunc is_failing():\n\tif(gut.get_current_test_object() != null and\n\t\t!['before_all', 'after_all'].has(gut.get_current_test_object().name)):\n\n\t\treturn gut.get_current_test_object().is_failing()\n\telse:\n\t\t_lgr.error('No current test object found.  is_failing must be called inside a test.')\n\t\treturn null\n\n# ------------------------------------------------------------------------------\n# Marks the test as passing.  Does not override any failing asserts or calls to\n# fail_test.  Same as a passing assert.\n# ------------------------------------------------------------------------------\nfunc pass_test(text):\n\t_pass(text)\n\n# ------------------------------------------------------------------------------\n# Marks the test as failing.  Same as a failing assert.\n# ------------------------------------------------------------------------------\nfunc fail_test(text):\n\t_fail(text)\n\n# ------------------------------------------------------------------------------\n# Peforms a deep compare on both values, a CompareResult instnace is returned.\n# The optional max_differences paramter sets the max_differences to be displayed.\n# ------------------------------------------------------------------------------\nfunc compare_deep(v1, v2, max_differences=null):\n\tvar result = _compare.deep(v1, v2)\n\tif(max_differences != null):\n\t\tresult.max_differences = max_differences\n\treturn result\n\n# ------------------------------------------------------------------------------\n# REMOVED\n# ------------------------------------------------------------------------------\nfunc compare_shallow(v1, v2, max_differences=null):\n\t_fail('compare_shallow has been removed.  Use compare_deep or just compare using == instead.')\n\t_lgr.error('compare_shallow has been removed.  Use compare_deep or just compare using == instead.')\n\treturn null\n\n\n# ------------------------------------------------------------------------------\n# Performs a deep compare and asserts the  values are equal\n# ------------------------------------------------------------------------------\nfunc assert_eq_deep(v1, v2):\n\tvar result = compare_deep(v1, v2)\n\tif(result.are_equal):\n\t\t_pass(result.get_short_summary())\n\telse:\n\t\t_fail(result.summary)\n\n# ------------------------------------------------------------------------------\n# Performs a deep compare and asserts the values are not equal\n# ------------------------------------------------------------------------------\nfunc assert_ne_deep(v1, v2):\n\tvar result = compare_deep(v1, v2)\n\tif(!result.are_equal):\n\t\t_pass(result.get_short_summary())\n\telse:\n\t\t_fail(result.get_short_summary())\n\n# ------------------------------------------------------------------------------\n# REMOVED\n# ------------------------------------------------------------------------------\nfunc assert_eq_shallow(v1, v2):\n\t_fail('assert_eq_shallow has been removed.  Use assert_eq/assert_same/assert_eq_deep')\n\n# ------------------------------------------------------------------------------\n# REMOVED\n# ------------------------------------------------------------------------------\nfunc assert_ne_shallow(v1, v2):\n\t_fail('assert_eq_shallow has been removed.  Use assert_eq/assert_same/assert_eq_deep')\n\n\n# ------------------------------------------------------------------------------\n# Assert wrapper for is_same\n# ------------------------------------------------------------------------------\nfunc assert_same(v1, v2, text=''):\n\tvar disp = \"[\" + _str(v1) + \"] expected to be same as  [\" + _str(v2) + \"]:  \" + text\n\tif(is_same(v1, v2)):\n\t\t_pass(disp)\n\telse:\n\t\t_fail(disp)\n\nfunc assert_not_same(v1, v2, text=''):\n\tvar disp = \"[\" + _str(v1) + \"] expected to not be same as  [\" + _str(v2) + \"]:  \" + text\n\tif(is_same(v1, v2)):\n\t\t_fail(disp)\n\telse:\n\t\t_pass(disp)\n\n# ------------------------------------------------------------------------------\n# Checks the passed in version string (x.x.x) against the engine version to see\n# if the engine version is less than the expected version.  If it is then the\n# test is mareked as passed (for a lack of anything better to do).  The result\n# of the check is returned.\n#\n# Example:\n# if(skip_if_godot_version_lt('3.5.0')):\n# \treturn\n# ------------------------------------------------------------------------------\nfunc skip_if_godot_version_lt(expected):\n\tvar should_skip = !GutUtils.is_godot_version_gte(expected)\n\tif(should_skip):\n\t\t_pass(str('Skipping: ', GutUtils.godot_version_string(), ' is less than ', expected))\n\treturn should_skip\n\n\n# ------------------------------------------------------------------------------\n# Checks if the passed in version matches the engine version.  The passed in\n# version can contain just the major, major.minor or major.minor.path.  If\n# the version is not the same then the test is marked as passed.  The result of\n# the check is returned.\n#\n# Example:\n# if(skip_if_godot_version_ne('3.4')):\n# \treturn\n# ------------------------------------------------------------------------------\nfunc skip_if_godot_version_ne(expected):\n\tvar should_skip = !GutUtils.is_godot_version(expected)\n\tif(should_skip):\n\t\t_pass(str('Skipping: ', GutUtils.godot_version_string(), ' is not ', expected))\n\treturn should_skip\n\n\n# ------------------------------------------------------------------------------\n# Registers all the inner classes in a script with the doubler.  This is required\n# before you can double any inner class.\n# ------------------------------------------------------------------------------\nfunc register_inner_classes(base_script):\n\tgut.get_doubler().inner_class_registry.register(base_script)\n"
  },
  {
    "path": "addons/gut/test.gd.uid",
    "content": "uid://bltyjx3s3d5ki\n"
  },
  {
    "path": "addons/gut/test_collector.gd",
    "content": "# ------------------------------------------------------------------------------\n# This class handles calling out to the test parser and maintaining an array of\n# collected_script.gd.  This is used for both calling the tests and tracking\n# the results of each script and test's execution.\n#\n# This also handles exporting and importing tests.\n# ------------------------------------------------------------------------------\nvar CollectedScript = GutUtils.CollectedScript\nvar CollectedTest = GutUtils.CollectedTest\n\nvar _test_prefix = 'test_'\nvar _test_class_prefix = 'Test'\n\nvar _lgr = GutUtils.get_logger()\n\n\n# Array of CollectedScripts.\nvar scripts = []\n\n\nfunc _does_inherit_from_test(thing):\n\tvar base_script = thing.get_base_script()\n\tvar to_return = false\n\tif(base_script != null):\n\t\tvar base_path = base_script.get_path()\n\t\tif(base_path == 'res://addons/gut/test.gd'):\n\t\t\tto_return = true\n\t\telse:\n\t\t\tto_return = _does_inherit_from_test(base_script)\n\treturn to_return\n\n\nfunc _populate_tests(test_script):\n\tvar script =  test_script.load_script()\n\tif(script == null):\n\t\tprint('  !!! ', test_script.path, ' could not be loaded')\n\t\treturn false\n\n\ttest_script.is_loaded = true\n\tvar methods = script.get_script_method_list()\n\tfor i in range(methods.size()):\n\t\tvar name = methods[i]['name']\n\t\tif(name.begins_with(_test_prefix)):\n\t\t\tvar t = CollectedTest.new()\n\t\t\tt.name = name\n\t\t\tt.arg_count = methods[i]['args'].size()\n\t\t\ttest_script.tests.append(t)\n\n\nfunc _get_inner_test_class_names(loaded):\n\tvar inner_classes = []\n\tvar const_map = loaded.get_script_constant_map()\n\tfor key in const_map:\n\t\tvar thing = const_map[key]\n\t\tif(GutUtils.is_gdscript(thing)):\n\t\t\tif(key.begins_with(_test_class_prefix)):\n\t\t\t\tif(_does_inherit_from_test(thing)):\n\t\t\t\t\tinner_classes.append(key)\n\t\t\t\telse:\n\t\t\t\t\t_lgr.warn(str('Ignoring Inner Class ', key,\n\t\t\t\t\t\t' because it does not extend GutTest'))\n\n\t\t\t# This could go deeper and find inner classes within inner classes\n\t\t\t# but requires more experimentation.  Right now I'm keeping it at\n\t\t\t# one level since that is what the previous version did and there\n\t\t\t# has been no demand for deeper nesting.\n\t\t\t# _populate_inner_test_classes(thing)\n\treturn inner_classes\n\n\nfunc _parse_script(test_script):\n\tvar inner_classes = []\n\tvar scripts_found = []\n\n\tvar loaded = GutUtils.WarningsManager.load_script_using_custom_warnings(\n\t\ttest_script.path,\n\t\tGutUtils.warnings_when_loading_test_scripts)\n\n\tif(_does_inherit_from_test(loaded)):\n\t\t_populate_tests(test_script)\n\t\tscripts_found.append(test_script.path)\n\t\tinner_classes = _get_inner_test_class_names(loaded)\n\telse:\n\t\treturn []\n\n\tfor i in range(inner_classes.size()):\n\t\tvar loaded_inner = loaded.get(inner_classes[i])\n\t\tif(_does_inherit_from_test(loaded_inner)):\n\t\t\tvar ts = CollectedScript.new(_lgr)\n\t\t\tts.path = test_script.path\n\t\t\tts.inner_class_name = inner_classes[i]\n\t\t\t_populate_tests(ts)\n\t\t\tscripts.append(ts)\n\t\t\tscripts_found.append(test_script.path + '[' + inner_classes[i] +']')\n\n\treturn scripts_found\n\n\n# -----------------\n# Public\n# -----------------\nfunc add_script(path):\n\t# SHORTCIRCUIT\n\tif(has_script(path)):\n\t\treturn []\n\n\t# SHORTCIRCUIT\n\tif(!FileAccess.file_exists(path)):\n\t\t# This check was added so tests could create dynmaic scripts and add\n\t\t# them to be run through gut.  This helps cut down on creating test\n\t\t# scripts to be used in test/resources.\n\t\tif(ResourceLoader.has_cached(path)):\n\t\t\t_lgr.debug(\"Using cached version of \" + path)\n\t\telse:\n\t\t\t_lgr.error('Could not find script:  ' + path)\n\t\t\treturn\n\n\tvar ts = CollectedScript.new(_lgr)\n\tts.path = path\n\t# Append right away because if we don't test_doubler.gd.TestInitParameters\n\t# will HARD crash.  I couldn't figure out what was causing the issue but\n\t# appending right away, and then removing if it's not valid seems to fix\n\t# things.  It might have to do with the ordering of the test classes in\n\t# the test collecter.  I'm not really sure.\n\tscripts.append(ts)\n\tvar parse_results = _parse_script(ts)\n\n\tif(parse_results.find(path) == -1):\n\t\t_lgr.warn(str('Ignoring script ', path, ' because it does not extend GutTest'))\n\t\tscripts.remove_at(scripts.find(ts))\n\n\treturn parse_results\n\n\nfunc clear():\n\tscripts.clear()\n\n\nfunc has_script(path):\n\tvar found = false\n\tvar idx = 0\n\twhile(idx < scripts.size() and !found):\n\t\tif(scripts[idx].get_full_name() == path):\n\t\t\tfound = true\n\t\telse:\n\t\t\tidx += 1\n\treturn found\n\n\nfunc export_tests(path):\n\tvar success = true\n\tvar f = ConfigFile.new()\n\tfor i in range(scripts.size()):\n\t\tscripts[i].export_to(f, str('CollectedScript-', i))\n\tvar result = f.save(path)\n\tif(result != OK):\n\t\t_lgr.error(str('Could not save exported tests to [', path, '].  Error code:  ', result))\n\t\tsuccess = false\n\treturn success\n\n\nfunc import_tests(path):\n\tvar success = false\n\tvar f = ConfigFile.new()\n\tvar result = f.load(path)\n\tif(result != OK):\n\t\t_lgr.error(str('Could not load exported tests from [', path, '].  Error code:  ', result))\n\telse:\n\t\tvar sections = f.get_sections()\n\t\tfor key in sections:\n\t\t\tvar ts = CollectedScript.new(_lgr)\n\t\t\tts.import_from(f, key)\n\t\t\t_populate_tests(ts)\n\t\t\tscripts.append(ts)\n\t\tsuccess = true\n\treturn success\n\n\nfunc get_script_named(name):\n\treturn GutUtils.search_array(scripts, 'get_filename_and_inner', name)\n\n\nfunc get_test_named(script_name, test_name):\n\tvar s = get_script_named(script_name)\n\tif(s != null):\n\t\treturn s.get_test_named(test_name)\n\telse:\n\t\treturn null\n\n\nfunc to_s():\n\tvar to_return = ''\n\tfor i in range(scripts.size()):\n\t\tto_return += scripts[i].to_s() + \"\\n\"\n\treturn to_return\n\n# ---------------------\n# Accessors\n# ---------------------\nfunc get_logger():\n\treturn _lgr\n\n\nfunc set_logger(logger):\n\t_lgr = logger\n\n\nfunc get_test_prefix():\n\treturn _test_prefix\n\n\nfunc set_test_prefix(test_prefix):\n\t_test_prefix = test_prefix\n\n\nfunc get_test_class_prefix():\n\treturn _test_class_prefix\n\n\nfunc set_test_class_prefix(test_class_prefix):\n\t_test_class_prefix = test_class_prefix\n\n\nfunc get_scripts():\n\treturn scripts\n\n\nfunc get_ran_test_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.get_ran_test_count()\n\treturn count\n\n\nfunc get_ran_script_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tif(s.was_run):\n\t\t\tcount += 1\n\treturn count\n\nfunc get_test_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.tests.size()\n\treturn count\n\n\nfunc get_assert_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.get_assert_count()\n\treturn count\n\n\nfunc get_pass_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.get_pass_count()\n\treturn count\n\n\nfunc get_fail_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.get_fail_count()\n\treturn count\n\n\nfunc get_pending_count():\n\tvar count = 0\n\tfor s in scripts:\n\t\tcount += s.get_pending_count()\n\treturn count\n\n"
  },
  {
    "path": "addons/gut/test_collector.gd.uid",
    "content": "uid://cfmqqrebag4me\n"
  },
  {
    "path": "addons/gut/thing_counter.gd",
    "content": "var things = {}\n\nfunc get_unique_count():\n\treturn things.size()\n\n\nfunc add_thing_to_count(thing):\n\tif(!things.has(thing)):\n\t\tthings[thing] = 0\n\n\nfunc add(thing):\n\tif(things.has(thing)):\n\t\tthings[thing] += 1\n\telse:\n\t\tthings[thing] = 1\n\n\nfunc has(thing):\n\treturn things.has(thing)\n\n\nfunc count(thing):\n\tvar to_return = 0\n\tif(things.has(thing)):\n\t\tto_return = things[thing]\n\treturn to_return\n\n\nfunc sum():\n\tvar to_return = 0\n\tfor key in things:\n\t\tto_return += things[key]\n\treturn to_return\n\n\nfunc to_s():\n\tvar to_return = \"\"\n\tfor key in things:\n\t\tto_return += str(key, \":  \", things[key], \"\\n\")\n\tto_return += str(\"sum: \", sum())\n\treturn to_return\n\n\nfunc get_max_count():\n\tvar max_val = null\n\tfor key in things:\n\t\tif(max_val == null or things[key] > max_val):\n\t\t\tmax_val = things[key]\n\treturn max_val\n\n\nfunc add_array_items(array):\n\tfor i in range(array.size()):\n\t\tadd(array[i])\n"
  },
  {
    "path": "addons/gut/thing_counter.gd.uid",
    "content": "uid://bgkixyp3db8wb\n"
  },
  {
    "path": "addons/gut/utils.gd",
    "content": "@tool\nclass_name GutUtils\nextends Object\n\nconst GUT_METADATA = '__gutdbl'\n\n# Note, these cannot change since places are checking for TYPE_INT to determine\n# how to process parameters.\nenum DOUBLE_STRATEGY{\n\tINCLUDE_NATIVE,\n\tSCRIPT_ONLY,\n}\n\nenum DIFF {\n\tDEEP,\n\tSIMPLE\n}\n\nconst TEST_STATUSES = {\n\tNO_ASSERTS = 'no asserts',\n\tSKIPPED = 'skipped',\n\tNOT_RUN = 'not run',\n\tPENDING = 'pending',\n\t# These two got the \"ed\" b/c pass is a reserved word and I could not\n\t# think of better words.\n\tFAILED = 'fail',\n\tPASSED = 'pass'\n}\n\nconst DOUBLE_TEMPLATES = {\n\tFUNCTION = 'res://addons/gut/double_templates/function_template.txt',\n\tINIT = 'res://addons/gut/double_templates/init_template.txt',\n\tSCRIPT = 'res://addons/gut/double_templates/script_template.txt',\n}\n\n\nstatic var GutScene = load('res://addons/gut/GutScene.tscn')\nstatic var LazyLoader = load('res://addons/gut/lazy_loader.gd')\nstatic var VersionNumbers = load(\"res://addons/gut/version_numbers.gd\")\nstatic var WarningsManager = load(\"res://addons/gut/warnings_manager.gd\")\n# --------------------------------\n# Lazy loaded scripts.  These scripts are lazy loaded so that they can be\n# declared, but will not load when this script is loaded.  This gives us a\n# window at the start of a run to adjust warning levels prior to loading\n# everything.\n# --------------------------------\nstatic var AutoFree = LazyLoader.new('res://addons/gut/autofree.gd'):\n\tget: return AutoFree.get_loaded()\n\tset(val): pass\nstatic var Awaiter = LazyLoader.new('res://addons/gut/awaiter.gd'):\n\tget: return Awaiter.get_loaded()\n\tset(val): pass\nstatic var Comparator = LazyLoader.new('res://addons/gut/comparator.gd'):\n\tget: return Comparator.get_loaded()\n\tset(val): pass\nstatic var CollectedTest = LazyLoader.new('res://addons/gut/collected_test.gd'):\n\tget: return CollectedTest.get_loaded()\n\tset(val): pass\nstatic var CollectedScript = LazyLoader.new('res://addons/gut/collected_script.gd'):\n\tget: return CollectedScript.get_loaded()\n\tset(val): pass\nstatic var CompareResult = LazyLoader.new('res://addons/gut/compare_result.gd'):\n\tget: return CompareResult.get_loaded()\n\tset(val): pass\nstatic var DiffTool = LazyLoader.new('res://addons/gut/diff_tool.gd'):\n\tget: return DiffTool.get_loaded()\n\tset(val): pass\nstatic var Doubler = LazyLoader.new('res://addons/gut/doubler.gd'):\n\tget: return Doubler.get_loaded()\n\tset(val): pass\nstatic var DynamicGdScript = LazyLoader.new(\"res://addons/gut/dynamic_gdscript.gd\") :\n\tget: return DynamicGdScript.get_loaded()\n\tset(val): pass\nstatic var Gut = LazyLoader.new('res://addons/gut/gut.gd'):\n\tget: return Gut.get_loaded()\n\tset(val): pass\nstatic var GutConfig = LazyLoader.new('res://addons/gut/gut_config.gd'):\n\tget: return GutConfig.get_loaded()\n\tset(val): pass\nstatic var HookScript = LazyLoader.new('res://addons/gut/hook_script.gd'):\n\tget: return HookScript.get_loaded()\n\tset(val): pass\nstatic var InnerClassRegistry = LazyLoader.new('res://addons/gut/inner_class_registry.gd'):\n\tget: return InnerClassRegistry.get_loaded()\n\tset(val): pass\nstatic var InputFactory = LazyLoader.new(\"res://addons/gut/input_factory.gd\"):\n\tget: return InputFactory.get_loaded()\n\tset(val): pass\nstatic var InputSender = LazyLoader.new(\"res://addons/gut/input_sender.gd\"):\n\tget: return InputSender.get_loaded()\n\tset(val): pass\nstatic var JunitXmlExport = LazyLoader.new('res://addons/gut/junit_xml_export.gd'):\n\tget: return JunitXmlExport.get_loaded()\n\tset(val): pass\nstatic var Logger = LazyLoader.new('res://addons/gut/logger.gd') : # everything should use get_logger\n\tget: return Logger.get_loaded()\n\tset(val): pass\nstatic var MethodMaker = LazyLoader.new('res://addons/gut/method_maker.gd'):\n\tget: return MethodMaker.get_loaded()\n\tset(val): pass\nstatic var OneToMany = LazyLoader.new('res://addons/gut/one_to_many.gd'):\n\tget: return OneToMany.get_loaded()\n\tset(val): pass\nstatic var OrphanCounter = LazyLoader.new('res://addons/gut/orphan_counter.gd'):\n\tget: return OrphanCounter.get_loaded()\n\tset(val): pass\nstatic var ParameterFactory = LazyLoader.new('res://addons/gut/parameter_factory.gd'):\n\tget: return ParameterFactory.get_loaded()\n\tset(val): pass\nstatic var ParameterHandler = LazyLoader.new('res://addons/gut/parameter_handler.gd'):\n\tget: return ParameterHandler.get_loaded()\n\tset(val): pass\nstatic var Printers = LazyLoader.new('res://addons/gut/printers.gd'):\n\tget: return Printers.get_loaded()\n\tset(val): pass\nstatic var ResultExporter = LazyLoader.new('res://addons/gut/result_exporter.gd'):\n\tget: return ResultExporter.get_loaded()\n\tset(val): pass\nstatic var ScriptCollector = LazyLoader.new('res://addons/gut/script_parser.gd'):\n\tget: return ScriptCollector.get_loaded()\n\tset(val): pass\nstatic var Spy = LazyLoader.new('res://addons/gut/spy.gd'):\n\tget: return Spy.get_loaded()\n\tset(val): pass\nstatic var Strutils = LazyLoader.new('res://addons/gut/strutils.gd'):\n\tget: return Strutils.get_loaded()\n\tset(val): pass\nstatic var Stubber = LazyLoader.new('res://addons/gut/stubber.gd'):\n\tget: return Stubber.get_loaded()\n\tset(val): pass\nstatic var StubParams = LazyLoader.new('res://addons/gut/stub_params.gd'):\n\tget: return StubParams.get_loaded()\n\tset(val): pass\nstatic var Summary = LazyLoader.new('res://addons/gut/summary.gd'):\n\tget: return Summary.get_loaded()\n\tset(val): pass\nstatic var Test = LazyLoader.new('res://addons/gut/test.gd'):\n\tget: return Test.get_loaded()\n\tset(val): pass\nstatic var TestCollector = LazyLoader.new('res://addons/gut/test_collector.gd'):\n\tget: return TestCollector.get_loaded()\n\tset(val): pass\nstatic var ThingCounter = LazyLoader.new('res://addons/gut/thing_counter.gd'):\n\tget: return ThingCounter.get_loaded()\n\tset(val): pass\n# --------------------------------\n\nstatic var avail_fonts = ['AnonymousPro', 'CourierPrime', 'LobsterTwo', 'Default']\n\nstatic var version_numbers = VersionNumbers.new(\n\t# gut_versrion (source of truth)\n\t'9.3.0',\n\t# required_godot_version\n\t'4.2.0'\n)\n\n\nstatic var warnings_at_start := { # WarningsManager dictionary\n\texclude_addons = true\n}\n\nstatic var warnings_when_loading_test_scripts := { # WarningsManager dictionary\n\tenable = false\n}\n\n\n# ------------------------------------------------------------------------------\n# Everything should get a logger through this.\n#\n# When running in test mode this will always return a new logger so that errors\n# are not caused by getting bad warn/error/etc counts.\n# ------------------------------------------------------------------------------\nstatic var _test_mode = false\nstatic var _lgr = null\nstatic func get_logger():\n\tif(_test_mode):\n\t\treturn Logger.new()\n\telse:\n\t\tif(_lgr == null):\n\t\t\t_lgr = Logger.new()\n\t\treturn _lgr\n\n\nstatic var _dyn_gdscript = DynamicGdScript.new()\nstatic func create_script_from_source(source, override_path=null):\n\tvar DynamicScript = _dyn_gdscript.create_script_from_source(source, override_path)\n\n\tif(typeof(DynamicScript) == TYPE_INT):\n\t\tvar l = get_logger()\n\t\tl.error(str('Could not create script from source.  Error:  ', DynamicScript))\n\t\tl.info(str(\"Source Code:\\n\", add_line_numbers(source)))\n\n\treturn DynamicScript\n\n\nstatic func godot_version_string():\n\treturn version_numbers.make_godot_version_string()\n\n\nstatic func is_godot_version(expected):\n\treturn VersionNumbers.VerNumTools.is_godot_version_eq(expected)\n\n\nstatic func is_godot_version_gte(expected):\n\treturn VersionNumbers.VerNumTools.is_godot_version_gte(expected)\n\n\nconst INSTALL_OK_TEXT = 'Everything checks out'\nstatic func make_install_check_text(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers):\n\tvar text = INSTALL_OK_TEXT\n\tif(!FileAccess.file_exists(template_paths.FUNCTION) or\n\t\t!FileAccess.file_exists(template_paths.INIT) or\n\t\t!FileAccess.file_exists(template_paths.SCRIPT)):\n\n\t\ttext = 'One or more GUT template files are missing.  If this is an exported project, you must include *.txt files in the export to run GUT.  If it is not an exported project then reinstall GUT.'\n\telif(!ver_nums.is_godot_version_valid()):\n\t\ttext = ver_nums.get_bad_version_text()\n\n\treturn text\n\n\nstatic func is_install_valid(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers):\n\treturn make_install_check_text(template_paths, ver_nums) == INSTALL_OK_TEXT\n\n\n# ------------------------------------------------------------------------------\n# Gets the root node without having to be in the tree and pushing out an error\n# if we don't have a main loop ready to go yet.\n# ------------------------------------------------------------------------------\n# static func get_root_node():\n# \tvar main_loop = Engine.get_main_loop()\n# \tif(main_loop != null):\n# \t\treturn main_loop.root\n# \telse:\n# \t\tpush_error('No Main Loop Yet')\n# \t\treturn null\n\n\n# ------------------------------------------------------------------------------\n# Gets the value from an enum.\n# - If passed an integer value as a string it will convert it to an int and\n# \tprocesses the int value.\n# - If the value is a float then it is converted to an int and then processes\n#\tthe int value\n# - If the value is an int, or was converted to an int, then the enum is checked\n#\tto see if it contains the value, if so then the value is returned.\n#\tOtherwise the default is returned.\n# - If the value is a string then it is uppercased and all spaces are replaced\n#\twith underscores.  It then checks to see if enum contains a key of that\n#\tname.  If so then the value for that key is returned, otherwise the default\n#\tis returned.\n#\n# This description is longer than the code, you should have just read the code\n# and the tests.\n# ------------------------------------------------------------------------------\nstatic func get_enum_value(thing, e, default=null):\n\tvar to_return = default\n\n\tif(typeof(thing) == TYPE_STRING and str(thing.to_int()) == thing):\n\t\tthing = thing.to_int()\n\telif(typeof(thing) == TYPE_FLOAT):\n\t\tthing = int(thing)\n\n\tif(typeof(thing) == TYPE_STRING):\n\t\tvar converted = thing.to_upper().replace(' ', '_')\n\t\tif(e.keys().has(converted)):\n\t\t\tto_return = e[converted]\n\telse:\n\t\tif(e.values().has(thing)):\n\t\t\tto_return = thing\n\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# return if_null if value is null otherwise return value\n# ------------------------------------------------------------------------------\nstatic func nvl(value, if_null):\n\tif(value == null):\n\t\treturn if_null\n\telse:\n\t\treturn value\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nstatic func pretty_print(dict, indent = '  '):\n\tprint(JSON.stringify(dict, indent))\n\n\n# ------------------------------------------------------------------------------\n# ------------------------------------------------------------------------------\nstatic func print_properties(props, thing, print_all_meta=false):\n\tfor i in range(props.size()):\n\t\tvar prop_name = props[i].name\n\t\tvar prop_value = thing.get(props[i].name)\n\t\tvar print_value = str(prop_value)\n\t\tif(print_value.length() > 100):\n\t\t\tprint_value = print_value.substr(0, 97) + '...'\n\t\telif(print_value == ''):\n\t\t\tprint_value = 'EMPTY'\n\n\t\tprint(prop_name, ' = ', print_value)\n\t\tif(print_all_meta):\n\t\t\tprint('  ', props[i])\n\n\n\n# ------------------------------------------------------------------------------\n# Gets the value of the node_property 'script' from a PackedScene's root node.\n# This does not assume the location of the root node in the PackedScene's node\n# list.  This also does not assume the index of the 'script' node property in\n# a nodes's property list.\n# ------------------------------------------------------------------------------\nstatic func get_scene_script_object(scene):\n\tvar state = scene.get_state()\n\tvar to_return = null\n\tvar root_node_path = NodePath(\".\")\n\tvar node_idx = 0\n\n\twhile(node_idx < state.get_node_count() and to_return == null):\n\t\tif(state.get_node_path(node_idx) == root_node_path):\n\t\t\tfor i in range(state.get_node_property_count(node_idx)):\n\t\t\t\tif(state.get_node_property_name(node_idx, i) == 'script'):\n\t\t\t\t\tto_return = state.get_node_property_value(node_idx, i)\n\n\t\tnode_idx += 1\n\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# returns true if the object has been freed, false if not\n#\n# From what i've read, the weakref approach should work.  It seems to work most\n# of the time but sometimes it does not catch it.  The str comparison seems to\n# fill in the gaps.  I've not seen any errors after adding that check.\n# ------------------------------------------------------------------------------\nstatic func is_freed(obj):\n\tvar wr = weakref(obj)\n\treturn !(wr.get_ref() and str(obj) != '<Freed Object>')\n\n\n# ------------------------------------------------------------------------------\n# Pretty self explanitory.\n# ------------------------------------------------------------------------------\nstatic func is_not_freed(obj):\n\treturn !is_freed(obj)\n\n\n# ------------------------------------------------------------------------------\n# Checks if the passed in object is a GUT Double or Partial Double.\n# ------------------------------------------------------------------------------\nstatic func is_double(obj):\n\tvar to_return = false\n\tif(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)):\n\t\tto_return = obj.has_method('__gutdbl_check_method__')\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# Checks an object to see if it is a GDScriptNativeClass\n# ------------------------------------------------------------------------------\nstatic func is_native_class(thing):\n\tvar it_is = false\n\tif(typeof(thing) == TYPE_OBJECT):\n\t\tit_is = str(thing).begins_with(\"<GDScriptNativeClass#\")\n\treturn it_is\n\n\n# ------------------------------------------------------------------------------\n# Checks if the passed in is an instance of a class\n# ------------------------------------------------------------------------------\nstatic func is_instance(obj):\n\treturn typeof(obj) == TYPE_OBJECT and \\\n\t\t!is_native_class(obj) and \\\n\t\t!obj.has_method('new') and \\\n\t\t!obj.has_method('instantiate')\n\n\n# ------------------------------------------------------------------------------\n# Checks if the passed in is a GDScript\n# ------------------------------------------------------------------------------\nstatic func is_gdscript(obj):\n\treturn typeof(obj) == TYPE_OBJECT and str(obj).begins_with('<GDScript#')\n\n\n# ------------------------------------------------------------------------------\n# Checks if the passed in is an inner class\n#\n# Looks like the resource_path will be populated for gdscripts, and not populated\n# for gdscripts inside a gdscript.\n# ------------------------------------------------------------------------------\nstatic func is_inner_class(obj):\n\treturn is_gdscript(obj) and obj.resource_path == ''\n\n\n# ------------------------------------------------------------------------------\n# Returns an array of values by calling get(property) on each element in source\n# ------------------------------------------------------------------------------\nstatic func extract_property_from_array(source, property):\n\tvar to_return = []\n\tfor i in (source.size()):\n\t\tto_return.append(source[i].get(property))\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# true if what is passed in is null or an empty string.\n# ------------------------------------------------------------------------------\nstatic func is_null_or_empty(text):\n\treturn text == null or text == ''\n\n\n# ------------------------------------------------------------------------------\n# Get the name of a native class or null if the object passed in is not a\n# native class.\n# ------------------------------------------------------------------------------\nstatic func get_native_class_name(thing):\n\tvar to_return = null\n\tif(is_native_class(thing)):\n\t\tvar newone = thing.new()\n\t\tto_return = newone.get_class()\n\t\tif(!newone is RefCounted):\n\t\t\tnewone.free()\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# Write a file.\n# ------------------------------------------------------------------------------\nstatic func write_file(path, content):\n\tvar f = FileAccess.open(path, FileAccess.WRITE)\n\tif(f != null):\n\t\tf.store_string(content)\n\tf = null;\n\n\treturn FileAccess.get_open_error()\n\n\n# ------------------------------------------------------------------------------\n# Returns the text of a file or an empty string if the file could not be opened.\n# ------------------------------------------------------------------------------\nstatic func get_file_as_text(path):\n\tvar to_return = ''\n\tvar f = FileAccess.open(path, FileAccess.READ)\n\tif(f != null):\n\t\tto_return = f.get_as_text()\n\telse:\n\t\tvar err = FileAccess.get_open_error()\n\t\t_lgr.error(str('Could not open file ', path, '.  Error ', err))\n\tf = null\n\treturn to_return\n\n\n# ------------------------------------------------------------------------------\n# Loops through an array of things and calls a method or checks a property on\n# each element until it finds the returned value.  -1 is returned if not found\n# or the index is returned if found.\n# ------------------------------------------------------------------------------\nstatic func search_array_idx(ar, prop_method, value):\n\tvar found = false\n\tvar idx = 0\n\n\twhile(idx < ar.size() and !found):\n\t\tvar item = ar[idx]\n\t\tvar prop = item.get(prop_method)\n\t\tif(!(prop is Callable)):\n\t\t\tif(item.get(prop_method) == value):\n\t\t\t\tfound = true\n\t\telif(prop != null):\n\t\t\tvar called_val = prop.call()\n\t\t\tif(called_val == value):\n\t\t\t\tfound = true\n\n\t\tif(!found):\n\t\t\tidx += 1\n\n\tif(found):\n\t\treturn idx\n\telse:\n\t\treturn -1\n\n\n# ------------------------------------------------------------------------------\n# Loops through an array of things and calls a method or checks a property on\n# each element until it finds the returned value.  The item in the array is\n# returned or null if it is not found (this method originally came first).\n# ------------------------------------------------------------------------------\nstatic func search_array(ar, prop_method, value):\n\tvar idx = search_array_idx(ar, prop_method, value)\n\n\tif(idx != -1):\n\t\treturn ar[idx]\n\telse:\n\t\treturn null\n\n\nstatic func are_datatypes_same(got, expected):\n\treturn !(typeof(got) != typeof(expected) and got != null and expected != null)\n\n\nstatic func get_script_text(obj):\n\treturn obj.get_script().get_source_code()\n\n\n# func get_singleton_by_name(name):\n# \tvar source = str(\"var singleton = \", name)\n# \tvar script = GDScript.new()\n# \tscript.set_source_code(source)\n# \tscript.reload()\n# \treturn script.new().singleton\n\n\nstatic func dec2bistr(decimal_value, max_bits = 31):\n\tvar binary_string = \"\"\n\tvar temp\n\tvar count = max_bits\n\n\twhile(count >= 0):\n\t\ttemp = decimal_value >> count\n\t\tif(temp & 1):\n\t\t\tbinary_string = binary_string + \"1\"\n\t\telse:\n\t\t\tbinary_string = binary_string + \"0\"\n\t\tcount -= 1\n\n\treturn binary_string\n\n\nstatic func add_line_numbers(contents):\n\tif(contents == null):\n\t\treturn ''\n\n\tvar to_return = \"\"\n\tvar lines = contents.split(\"\\n\")\n\tvar line_num = 1\n\tfor line in lines:\n\t\tvar line_str = str(line_num).lpad(6, ' ')\n\t\tto_return += str(line_str, ' |', line, \"\\n\")\n\t\tline_num += 1\n\treturn to_return\n\n\nstatic func get_display_size():\n\treturn Engine.get_main_loop().get_viewport().get_visible_rect()\n\n\n\n\n\n# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ##############################################################################\n# The MIT License (MIT)\n# =====================\n#\n# Copyright (c) 2023 Tom \"Butch\" Wesley\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n#\n# ##############################################################################\n\n"
  },
  {
    "path": "addons/gut/utils.gd.uid",
    "content": "uid://vlnqona4hyj3\n"
  },
  {
    "path": "addons/gut/version_conversion.gd",
    "content": "class ConfigurationUpdater:\n\tvar EditorGlobals = load(\"res://addons/gut/gui/editor_globals.gd\")\n\n\tfunc warn(message):\n\t\tprint('GUT Warning:  ', message)\n\n\tfunc info(message):\n\t\tprint(\"GUT Info:  \", message)\n\n\tfunc moved_file(from, to):\n\t\tif(FileAccess.file_exists(from) and !FileAccess.file_exists(to)):\n\t\t\tinfo(str('Copying [', from, '] to [', to, ']'))\n\t\t\tvar result = DirAccess.copy_absolute(from, to)\n\t\t\tif(result != OK):\n\t\t\t\twarn(str('Could not copy [', from, '] to [', to, ']'))\n\n\t\tif(FileAccess.file_exists(from) and FileAccess.file_exists(to)):\n\t\t\twarn(str('File [', from, '] has been moved to [', to, \"].\\n    You can delete \", from))\n\n\n\tfunc move_user_file(from, to):\n\t\tif(from.begins_with('user://') and to.begins_with('user://')):\n\t\t\tif(FileAccess.file_exists(from) and !FileAccess.file_exists(to)):\n\t\t\t\tinfo(str('Moving [', from, '] to [', to, ']'))\n\t\t\t\tvar result = DirAccess.copy_absolute(from, to)\n\t\t\t\tif(result == OK):\n\t\t\t\t\tinfo(str('    ', 'Created ', to))\n\t\t\t\t\tresult = DirAccess.remove_absolute(from)\n\t\t\t\t\tif(result != OK):\n\t\t\t\t\t\twarn(str('    ', 'Could not delete ', from))\n\t\t\t\t\telse:\n\t\t\t\t\t\tinfo(str('    ', 'Deleted ', from))\n\t\t\t\telse:\n\t\t\t\t\twarn(str('    ', 'Could not copy [', from, '] to [', to, ']'))\n\t\telse:\n\t\t\twarn(str('Attempt to move_user_file with files not in user:// ', from, '->', to))\n\n\n\tfunc remove_user_file(which):\n\t\tif(which.begins_with('user://') and FileAccess.file_exists(which)):\n\t\t\tinfo(str('Deleting obsolete file ', which))\n\t\t\tvar result = DirAccess.remove_absolute(which)\n\t\t\tif(result != OK):\n\t\t\t\twarn(str('    ', 'Could not delete ', which))\n\t\t\telse:\n\t\t\t\tinfo(str('    ', 'Deleted ', which))\n\nclass v9_2_0:\n\textends ConfigurationUpdater\n\n\tfunc validate():\n\t\tmoved_file('res://.gut_editor_config.json', EditorGlobals.editor_run_gut_config_path)\n\t\tmoved_file('res://.gut_editor_shortcuts.cfg', EditorGlobals.editor_shortcuts_path)\n\t\tremove_user_file('user://.gut_editor.bbcode')\n\t\tremove_user_file('user://.gut_editor.json')\n\n\nstatic func convert():\n\tvar inst = v9_2_0.new()\n\tinst.validate()"
  },
  {
    "path": "addons/gut/version_conversion.gd.uid",
    "content": "uid://d3iqojilhr1fy\n"
  },
  {
    "path": "addons/gut/version_numbers.gd",
    "content": "# ##############################################################################\n#\n# ##############################################################################\nclass VerNumTools:\n\n\tstatic func _make_version_array_from_string(v):\n\t\tvar parts = Array(v.split('.'))\n\t\tfor i in range(parts.size()):\n\t\t\tvar int_val = parts[i].to_int()\n\t\t\tif(str(int_val) == parts[i]):\n\t\t\t\tparts[i] = parts[i].to_int()\n\t\treturn parts\n\n\n\tstatic func make_version_array(v):\n\t\tvar to_return = []\n\t\tif(typeof(v) == TYPE_STRING):\n\t\t\tto_return = _make_version_array_from_string(v)\n\t\telif(typeof(v) == TYPE_DICTIONARY):\n\t\t\treturn [v.major, v.minor, v.patch]\n\t\telif(typeof(v) == TYPE_ARRAY):\n\t\t\tto_return = v\n\t\treturn to_return\n\n\n\tstatic func make_version_string(version_parts):\n\t\tvar to_return = 'x.x.x'\n\t\tif(typeof(version_parts) == TYPE_ARRAY):\n\t\t\tto_return =  \".\".join(version_parts)\n\t\telif(typeof(version_parts) == TYPE_DICTIONARY):\n\t\t\tto_return = str(version_parts.major,  '.',  version_parts.minor,  '.',  version_parts.patch)\n\t\telif(typeof(version_parts) == TYPE_STRING):\n\t\t\tto_return = version_parts\n\t\treturn to_return\n\n\n\tstatic func is_version_gte(version, required):\n\t\tvar is_ok = null\n\t\tvar v = make_version_array(version)\n\t\tvar r = make_version_array(required)\n\n\t\tvar idx = 0\n\t\twhile(is_ok == null and idx < v.size() and idx < r.size()):\n\t\t\tif(v[idx] > r[idx]):\n\t\t\t\tis_ok = true\n\t\t\telif(v[idx] < r[idx]):\n\t\t\t\tis_ok = false\n\n\t\t\tidx += 1\n\n\t\t# still null means each index was the same.\n\t\treturn GutUtils.nvl(is_ok, true)\n\n\n\tstatic func is_version_eq(version, expected):\n\t\tvar version_array = make_version_array(version)\n\t\tvar expected_array = make_version_array(expected)\n\n\t\tif(expected_array.size() > version_array.size()):\n\t\t\treturn false\n\n\t\tvar is_version = true\n\t\tvar i = 0\n\t\twhile(i < expected_array.size() and i < version_array.size() and is_version):\n\t\t\tif(expected_array[i] == version_array[i]):\n\t\t\t\ti += 1\n\t\t\telse:\n\t\t\t\tis_version = false\n\n\t\treturn is_version\n\n\n\tstatic func is_godot_version_eq(expected):\n\t\treturn VerNumTools.is_version_eq(Engine.get_version_info(), expected)\n\n\n\tstatic func is_godot_version_gte(expected):\n\t\treturn VerNumTools.is_version_gte(Engine.get_version_info(), expected)\n\n\n\n\n# ##############################################################################\n#\n# ##############################################################################\nvar gut_version = '0.0.0'\nvar required_godot_version = '0.0.0'\n\nfunc _init(gut_v = gut_version, required_godot_v = required_godot_version):\n\tgut_version = gut_v\n\trequired_godot_version = required_godot_v\n\n\n# ------------------------------------------------------------------------------\n# Blurb of text with GUT and Godot versions.\n# ------------------------------------------------------------------------------\nfunc get_version_text():\n\tvar v_info = Engine.get_version_info()\n\tvar gut_version_info =  str('GUT version:  ', gut_version)\n\tvar godot_version_info  = str('Godot version:  ', v_info.major,  '.',  v_info.minor,  '.',  v_info.patch)\n\treturn godot_version_info + \"\\n\" + gut_version_info\n\n\n# ------------------------------------------------------------------------------\n# Returns a nice string for erroring out when we have a bad Godot version.\n# ------------------------------------------------------------------------------\nfunc get_bad_version_text():\n\tvar info = Engine.get_version_info()\n\tvar gd_version = str(info.major, '.', info.minor, '.', info.patch)\n\treturn 'GUT ' + gut_version + ' requires Godot ' + required_godot_version + \\\n\t\t' or greater.  Godot version is ' + gd_version\n\n\n# ------------------------------------------------------------------------------\n# Checks the Godot version against required_godot_version.\n# ------------------------------------------------------------------------------\nfunc is_godot_version_valid():\n\treturn VerNumTools.is_version_gte(Engine.get_version_info(), required_godot_version)\n\n\nfunc make_godot_version_string():\n\treturn VerNumTools.make_version_string(Engine.get_version_info())\n"
  },
  {
    "path": "addons/gut/version_numbers.gd.uid",
    "content": "uid://gltdh6t0um0\n"
  },
  {
    "path": "addons/gut/warnings_manager.gd",
    "content": "const IGNORE = 0\nconst WARN = 1\nconst ERROR = 2\n\n\nconst WARNING_LOOKUP = {\n\tIGNORE : 'IGNORE',\n\tWARN : 'WARN',\n\tERROR : 'ERROR'\n}\n\nconst GDSCRIPT_WARNING = 'debug/gdscript/warnings/'\n\n# ---------------------------------------\n# Static\n# ---------------------------------------\nstatic var _static_init_called = false\n# This is static and set in _static_init so that we can get the current settings as\n# soon as possible.\nstatic var _project_warnings : Dictionary = {}\nstatic var project_warnings := {} :\n\tget:\n\t\t# somehow this gets called before _project_warnings is initialized when\n\t\t# loading a project in the editor.  It causes an error stating that\n\t\t# duplicate can't be called on nil.  It seems there might be an\n\t\t# implicit \"get\" call happening.  Using push_error I saw a message\n\t\t# in this method, but not one from _static_init upon loading the project\n\t\tif(_static_init_called):\n\t\t\treturn _project_warnings.duplicate()\n\t\telse:\n\t\t\treturn {}\n\tset(val): pass\n\n\nstatic func _static_init():\n\t_project_warnings = create_warnings_dictionary_from_project_settings()\n\t_static_init_called = true\n\n\nstatic func are_warnings_enabled():\n\treturn ProjectSettings.get(str(GDSCRIPT_WARNING, 'enable'))\n\n\n## Turn all warnings on/off.  Use reset_warnings to restore the original value.\nstatic func enable_warnings(should=true):\n\tProjectSettings.set(str(GDSCRIPT_WARNING, 'enable'), should)\n\n\n## Turn on/off excluding addons.  Use reset_warnings to restore the original value.\nstatic func exclude_addons(should=true):\n\tProjectSettings.set(str(GDSCRIPT_WARNING, 'exclude_addons'), should)\n\n\n## Resets warning settings to what they are set to in Project Settings\nstatic func reset_warnings():\n\tapply_warnings_dictionary(_project_warnings)\n\n\n\nstatic func set_project_setting_warning(warning_name : String, value : Variant):\n\tvar property_name = str(GDSCRIPT_WARNING, warning_name)\n\t# This check will generate a warning if the setting does not exist\n\tif(property_name in ProjectSettings):\n\t\tProjectSettings.set(property_name, value)\n\n\nstatic func apply_warnings_dictionary(warning_values : Dictionary):\n\tfor key in warning_values:\n\t\tset_project_setting_warning(key, warning_values[key])\n\n# ---------------------------------------\n# Class\n# ---------------------------------------\nstatic func create_ignore_all_dictionary():\n\treturn replace_warnings_values(project_warnings, -1, IGNORE)\n\n\nstatic func create_warn_all_warnings_dictionary():\n\treturn replace_warnings_values(project_warnings, -1, WARN)\n\n\nstatic func replace_warnings_with_ignore(dict):\n\treturn replace_warnings_values(dict, WARN, IGNORE)\n\n\nstatic func replace_errors_with_warnings(dict):\n\treturn replace_warnings_values(dict, ERROR, WARN)\n\n\nstatic func replace_warnings_values(dict, replace_this, with_this):\n\tvar to_return = dict.duplicate()\n\tfor key in to_return:\n\t\tif(typeof(to_return[key]) == TYPE_INT and (replace_this == -1 or to_return[key] == replace_this)):\n\t\t\tto_return[key] = with_this\n\treturn to_return\n\n\nstatic func create_warnings_dictionary_from_project_settings() -> Dictionary :\n\tvar props = ProjectSettings.get_property_list()\n\tvar to_return = {}\n\tfor i in props.size():\n\t\tif(props[i].name.begins_with(GDSCRIPT_WARNING)):\n\t\t\tvar prop_name = props[i].name.replace(GDSCRIPT_WARNING, '')\n\t\t\tto_return[prop_name] = ProjectSettings.get(props[i].name)\n\treturn to_return\n\n\nstatic func print_warnings_dictionary(which : Dictionary):\n\tvar is_valid = true\n\tfor key in which:\n\t\tvar value_str = str(which[key])\n\t\tif(_project_warnings.has(key)):\n\t\t\tif(typeof(which[key]) == TYPE_INT):\n\t\t\t\tif(WARNING_LOOKUP.has(which[key])):\n\t\t\t\t\tvalue_str = WARNING_LOOKUP[which[key]]\n\t\t\t\telse:\n\t\t\t\t\tpush_warning(str(which[key], ' is not a valid value for ', key))\n\t\t\t\t\tis_valid = false\n\t\telse:\n\t\t\tpush_warning(str(key, ' is not a valid warning setting'))\n\t\t\tis_valid = false\n\t\tvar s = str(key, ' = ', value_str)\n\t\tprint(s)\n\treturn is_valid\n\n\nstatic func load_script_ignoring_all_warnings(path : String) -> Variant:\n\treturn load_script_using_custom_warnings(path, create_ignore_all_dictionary())\n\n\nstatic func load_script_using_custom_warnings(path : String, warnings_dictionary : Dictionary) -> Variant:\n\tvar current_warns = create_warnings_dictionary_from_project_settings()\n\tapply_warnings_dictionary(warnings_dictionary)\n\tvar s = load(path)\n\tapply_warnings_dictionary(current_warns)\n\n\treturn s\n"
  },
  {
    "path": "addons/gut/warnings_manager.gd.uid",
    "content": "uid://f5eifaw48jio\n"
  },
  {
    "path": "export_presets.cfg",
    "content": "[preset.0]\n\nname=\"Windows Desktop\"\nplatform=\"Windows Desktop\"\nrunnable=true\nadvanced_options=false\ndedicated_server=false\ncustom_features=\"\"\nexport_filter=\"all_resources\"\ninclude_filter=\"\"\nexclude_filter=\"\"\nexport_path=\"\"\npatches=PackedStringArray()\nencryption_include_filters=\"\"\nencryption_exclude_filters=\"\"\nseed=0\nencrypt_pck=false\nencrypt_directory=false\nscript_export_mode=2\n\n[preset.0.options]\n\ncustom_template/debug=\"\"\ncustom_template/release=\"\"\ndebug/export_console_wrapper=1\nbinary_format/embed_pck=false\ntexture_format/s3tc_bptc=true\ntexture_format/etc2_astc=false\nshader_baker/enabled=false\nbinary_format/architecture=\"x86_64\"\ncodesign/enable=false\ncodesign/timestamp=true\ncodesign/timestamp_server_url=\"\"\ncodesign/digest_algorithm=1\ncodesign/description=\"\"\ncodesign/custom_options=PackedStringArray()\napplication/modify_resources=true\napplication/icon=\"\"\napplication/console_wrapper_icon=\"\"\napplication/icon_interpolation=4\napplication/file_version=\"\"\napplication/product_version=\"\"\napplication/company_name=\"\"\napplication/product_name=\"\"\napplication/file_description=\"\"\napplication/copyright=\"\"\napplication/trademarks=\"\"\napplication/export_angle=0\napplication/export_d3d12=0\napplication/d3d12_agility_sdk_multiarch=true\nssh_remote_deploy/enabled=false\nssh_remote_deploy/host=\"user@host_ip\"\nssh_remote_deploy/port=\"22\"\nssh_remote_deploy/extra_args_ssh=\"\"\nssh_remote_deploy/extra_args_scp=\"\"\nssh_remote_deploy/run_script=\"Expand-Archive -LiteralPath '{temp_dir}\\\\{archive_name}' -DestinationPath '{temp_dir}'\r\n$action = New-ScheduledTaskAction -Execute '{temp_dir}\\\\{exe_name}' -Argument '{cmd_args}'\r\n$trigger = New-ScheduledTaskTrigger -Once -At 00:00\r\n$settings = New-ScheduledTaskSettingsSet\r\n$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings\r\nRegister-ScheduledTask godot_remote_debug -InputObject $task -Force:$true\r\nStart-ScheduledTask -TaskName godot_remote_debug\r\nwhile (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }\r\nUnregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue\"\nssh_remote_deploy/cleanup_script=\"Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue\r\nUnregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue\r\nRemove-Item -Recurse -Force '{temp_dir}'\"\n\n[preset.1]\n\nname=\"Linux\"\nplatform=\"Linux\"\nrunnable=true\nadvanced_options=false\ndedicated_server=false\ncustom_features=\"\"\nexport_filter=\"all_resources\"\ninclude_filter=\"\"\nexclude_filter=\"\"\nexport_path=\"\"\npatches=PackedStringArray()\nencryption_include_filters=\"\"\nencryption_exclude_filters=\"\"\nseed=0\nencrypt_pck=false\nencrypt_directory=false\nscript_export_mode=2\n\n[preset.1.options]\n\ncustom_template/debug=\"\"\ncustom_template/release=\"\"\ndebug/export_console_wrapper=1\nbinary_format/embed_pck=false\ntexture_format/s3tc_bptc=true\ntexture_format/etc2_astc=false\nshader_baker/enabled=false\nbinary_format/architecture=\"x86_64\"\nssh_remote_deploy/enabled=false\nssh_remote_deploy/host=\"user@host_ip\"\nssh_remote_deploy/port=\"22\"\nssh_remote_deploy/extra_args_ssh=\"\"\nssh_remote_deploy/extra_args_scp=\"\"\nssh_remote_deploy/run_script=\"#!/usr/bin/env bash\nexport DISPLAY=:0\nunzip -o -q \\\"{temp_dir}/{archive_name}\\\" -d \\\"{temp_dir}\\\"\n\\\"{temp_dir}/{exe_name}\\\" {cmd_args}\"\nssh_remote_deploy/cleanup_script=\"#!/usr/bin/env bash\nkill $(pgrep -x -f \\\"{temp_dir}/{exe_name} {cmd_args}\\\")\nrm -rf \\\"{temp_dir}\\\"\"\n"
  },
  {
    "path": "project.godot",
    "content": "; Engine configuration file.\n; It's best edited using the editor UI and not directly,\n; since the parameters that go here are not all obvious.\n;\n; Format:\n;   [section] ; section goes between []\n;   param=value ; assign values to parameters\n\nconfig_version=5\n\n_project_builder_config_path=\"project_builds_config.txt\"\n\n[application]\n\nconfig/name=\"Godot Project Builder\"\nconfig/tags=PackedStringArray(\"tool\")\nrun/main_scene=\"res://Scenes/ProjectManager.tscn\"\nconfig/features=PackedStringArray(\"4.5\")\nrun/low_processor_mode=true\nconfig/icon=\"res://Icons/Icon.png\"\n\n[autoload]\n\nData=\"*res://Scripts/Data.gd\"\n\n[debug]\n\ngdscript/warnings/unassigned_variable=0\ngdscript/warnings/unassigned_variable_op_assign=0\ngdscript/warnings/unused_variable=0\ngdscript/warnings/unused_local_constant=0\ngdscript/warnings/unused_private_class_variable=0\ngdscript/warnings/unused_parameter=0\ngdscript/warnings/unused_signal=0\ngdscript/warnings/unreachable_code=0\ngdscript/warnings/incompatible_ternary=0\ngdscript/warnings/integer_division=0\ngdscript/warnings/narrowing_conversion=0\ngdscript/warnings/confusable_local_declaration=0\n\n[display]\n\nwindow/size/viewport_width=1280\nwindow/size/viewport_height=720\nwindow/subwindows/embed_subwindows=false\n\n[editor]\n\nscript/search_in_file_extensions=PackedStringArray(\"gd\", \"gdshader\", \"tscn\")\nscript/templates_search_path=\"res://Scripts/Templates\"\n\n[editor_plugins]\n\nenabled=PackedStringArray(\"res://addons/ProjectBuilder/plugin.cfg\", \"res://addons/gut/plugin.cfg\")\n\n[rendering]\n\nrenderer/rendering_method=\"gl_compatibility\"\nenvironment/defaults/default_clear_color=Color(0.211765, 0.239216, 0.321569, 1)\n"
  },
  {
    "path": "project_builds_config.txt",
    "content": "{\n\"epic_artifact_id\": \"\",\n\"epic_cloud_dir\": \"\",\n\"epic_product_id\": \"\",\n\"godot_path\": \"\",\n\"itch_default_channel\": \"\",\n\"itch_game_name\": \"\",\n\"itch_version_file\": \"\",\n\"routines\": Array[Dictionary]([{\n\"name\": \"Export Windows\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"debug\": false,\n\"path_suffix\": \"Windows/Project Builder.exe\",\n\"preset\": \"Windows Desktop\",\n\"template\": \"Main\"\n},\n\"scene\": \"ExportProjectFromTemplate\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"addons/ProjectBuilder\",\n\"target_path\": \".export/Windows/addons/ProjectBuilder\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"Tasks\",\n\"target_path\": \".export/Windows/Tasks\"\n},\n\"scene\": \"CopyFiles\"\n}])\n}, {\n\"name\": \"Export Linux\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"debug\": false,\n\"path_suffix\": \"Linux/Project Builder.x86_64\",\n\"preset\": \"Linux\",\n\"template\": \"Main\"\n},\n\"scene\": \"ExportProjectFromTemplate\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"addons/ProjectBuilder\",\n\"target_path\": \".export/Linux/addons/ProjectBuilder\"\n},\n\"scene\": \"CopyFiles\"\n}, {\n\"data\": {\n\"recursive\": true,\n\"source_path\": \"Tasks\",\n\"target_path\": \".export/Linux/Tasks\"\n},\n\"scene\": \"CopyFiles\"\n}])\n}, {\n\"name\": \"Export All\",\n\"on_fail\": 0,\n\"tasks\": Array[Dictionary]([{\n\"data\": {\n\"routine\": \"Export Windows\"\n},\n\"scene\": \"SubRoutine\"\n}, {\n\"data\": {\n\"destination\": \".export/ProjectBuilderWindows.zip\",\n\"exclude_files\": \"\",\n\"include_files\": \"\",\n\"source\": \".export/Windows\"\n},\n\"scene\": \"PackZIP\"\n}, {\n\"data\": {\n\"routine\": \"Export Linux\"\n},\n\"scene\": \"SubRoutine\"\n}, {\n\"data\": {\n\"destination\": \".export/ProjectBuilderLinux.zip\",\n\"exclude_files\": \"\",\n\"include_files\": \"\",\n\"source\": \".export/Linux\"\n},\n\"scene\": \"PackZIP\"\n}])\n}]),\n\"templates\": Array[Dictionary]([{\n\"custom_features\": PackedStringArray(),\n\"exclude_filters\": PackedStringArray(\"Tasks/*\", \"addons/ProjectBuilder/*\", \"Tests/*\", \"addons/gut/*\"),\n\"export_path\": \".export\",\n\"include_filters\": PackedStringArray(),\n\"inherit\": \"\",\n\"name\": \"Main\"\n}])\n}"
  }
]