From c4beeae38f45a85bcfb2145f75e2f3f700f3f8d4 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 3 Sep 2026 16:03:57 +0200 Subject: [PATCH] Initial commit: Transcience MVP Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated server with client-side prediction. Clients send input only; the server resolves every hit for both players and enemies (no PvP). - SimWorld: whole simulation as plain RefCounted objects (no nodes, no physics server), ~0.24ms/tick at peak load -- runs headless for free and drives 78 tests in under a second - BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn events rather than per-tick state - Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash enemies and bosses -- a new boss is data in src/content/content.gd, no simulation changes - The Warden of the Fold: stationary 4-phase boss built entirely on that format - Lobby hub with a portal into on-demand dungeon instances; one process hosts the hub plus every concurrent dungeon - Emergency escape: 3s server-owned channel, cancelled by damage - tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over ENet), bench.gd; git hooks wired to the same scripts - docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md --- .claude/commands/newboss.md | 22 + .claude/commands/playtest.md | 14 + .claude/commands/verify.md | 15 + .claude/settings.json | 20 + .gitattributes | 5 + .githooks/pre-commit | 86 + .githooks/pre-push | 22 + .github/workflows/ci.yml | 30 + .gitignore | 10 + CLAUDE.md | 111 + README.md | 47 + addons/gut/GutScene.gd | 132 + addons/gut/GutScene.gd.uid | 1 + addons/gut/GutScene.tscn | 16 + addons/gut/LICENSE.md | 22 + addons/gut/UserFileViewer.gd | 52 + addons/gut/UserFileViewer.gd.uid | 1 + addons/gut/UserFileViewer.tscn | 92 + addons/gut/autofree.gd | 86 + addons/gut/autofree.gd.uid | 1 + addons/gut/awaiter.gd | 201 ++ addons/gut/awaiter.gd.uid | 1 + addons/gut/cli/change_project_warnings.gd | 239 ++ addons/gut/cli/change_project_warnings.gd.uid | 1 + addons/gut/cli/gut_cli.gd | 328 ++ addons/gut/cli/gut_cli.gd.uid | 1 + addons/gut/cli/optparse.gd | 678 ++++ addons/gut/cli/optparse.gd.uid | 1 + addons/gut/collected_script.gd | 208 ++ addons/gut/collected_script.gd.uid | 1 + addons/gut/collected_test.gd | 120 + addons/gut/collected_test.gd.uid | 1 + addons/gut/comparator.gd | 125 + addons/gut/comparator.gd.uid | 1 + addons/gut/compare_result.gd | 70 + addons/gut/compare_result.gd.uid | 1 + addons/gut/diff_formatter.gd | 63 + addons/gut/diff_formatter.gd.uid | 1 + addons/gut/diff_tool.gd | 156 + addons/gut/diff_tool.gd.uid | 1 + .../double_templates/double_data_template.txt | 22 + .../double_templates/function_template.txt | 9 + addons/gut/double_templates/init_template.txt | 4 + .../gut/double_templates/script_template.txt | 13 + .../double_templates/singleton_template.txt | 16 + addons/gut/double_tools.gd | 83 + addons/gut/double_tools.gd.uid | 1 + addons/gut/doubler.gd | 392 +++ addons/gut/doubler.gd.uid | 1 + addons/gut/dynamic_gdscript.gd | 33 + addons/gut/dynamic_gdscript.gd.uid | 1 + addons/gut/editor_caret_context_notifier.gd | 212 ++ .../gut/editor_caret_context_notifier.gd.uid | 1 + addons/gut/error_tracker.gd | 206 ++ addons/gut/error_tracker.gd.uid | 1 + addons/gut/fonts/AnonymousPro-Bold.ttf | Bin 0 -> 107624 bytes addons/gut/fonts/AnonymousPro-Bold.ttf.import | 36 + addons/gut/fonts/AnonymousPro-BoldItalic.ttf | Bin 0 -> 95872 bytes .../fonts/AnonymousPro-BoldItalic.ttf.import | 36 + addons/gut/fonts/AnonymousPro-Italic.ttf | Bin 0 -> 98424 bytes .../gut/fonts/AnonymousPro-Italic.ttf.import | 36 + addons/gut/fonts/AnonymousPro-Regular.ttf | Bin 0 -> 112072 bytes .../gut/fonts/AnonymousPro-Regular.ttf.import | 36 + addons/gut/fonts/CourierPrime-Bold.ttf | Bin 0 -> 69768 bytes addons/gut/fonts/CourierPrime-Bold.ttf.import | 36 + addons/gut/fonts/CourierPrime-BoldItalic.ttf | Bin 0 -> 77384 bytes .../fonts/CourierPrime-BoldItalic.ttf.import | 36 + addons/gut/fonts/CourierPrime-Italic.ttf | Bin 0 -> 76496 bytes .../gut/fonts/CourierPrime-Italic.ttf.import | 36 + addons/gut/fonts/CourierPrime-Regular.ttf | Bin 0 -> 68128 bytes .../gut/fonts/CourierPrime-Regular.ttf.import | 36 + addons/gut/fonts/LobsterTwo-Bold.ttf | Bin 0 -> 222040 bytes addons/gut/fonts/LobsterTwo-Bold.ttf.import | 36 + addons/gut/fonts/LobsterTwo-BoldItalic.ttf | Bin 0 -> 216036 bytes .../fonts/LobsterTwo-BoldItalic.ttf.import | 36 + addons/gut/fonts/LobsterTwo-Italic.ttf | Bin 0 -> 229404 bytes addons/gut/fonts/LobsterTwo-Italic.ttf.import | 36 + addons/gut/fonts/LobsterTwo-Regular.ttf | Bin 0 -> 234956 bytes .../gut/fonts/LobsterTwo-Regular.ttf.import | 36 + addons/gut/fonts/OFL.txt | 94 + addons/gut/get_editor_interface.gd | 6 + addons/gut/get_editor_interface.gd.uid | 1 + addons/gut/godot_singletons.gd | 50 + addons/gut/godot_singletons.gd.uid | 1 + addons/gut/gui/EditorRadioButton.tres | 13 + addons/gut/gui/GutBottomPanel.gd | 563 ++++ addons/gut/gui/GutBottomPanel.gd.uid | 1 + addons/gut/gui/GutBottomPanel.tscn | 321 ++ addons/gut/gui/GutControl.gd | 326 ++ addons/gut/gui/GutControl.gd.uid | 1 + addons/gut/gui/GutControl.tscn | 63 + addons/gut/gui/GutLogo.tscn | 36 + addons/gut/gui/GutRunner.gd | 247 ++ addons/gut/gui/GutRunner.gd.uid | 1 + addons/gut/gui/GutRunner.tscn | 12 + addons/gut/gui/GutSceneTheme.tres | 7 + addons/gut/gui/MinGui.tscn | 161 + addons/gut/gui/NormalGui.tscn | 215 ++ addons/gut/gui/OutputText.gd | 352 ++ addons/gut/gui/OutputText.gd.uid | 1 + addons/gut/gui/OutputText.tscn | 121 + addons/gut/gui/ResizeHandle.gd | 107 + addons/gut/gui/ResizeHandle.gd.uid | 1 + addons/gut/gui/ResizeHandle.tscn | 8 + addons/gut/gui/ResultsTree.gd | 368 +++ addons/gut/gui/ResultsTree.gd.uid | 1 + addons/gut/gui/ResultsTree.tscn | 34 + addons/gut/gui/RunAtCursor.gd | 171 + addons/gut/gui/RunAtCursor.gd.uid | 1 + addons/gut/gui/RunAtCursor.tscn | 60 + addons/gut/gui/RunExternally.gd | 216 ++ addons/gut/gui/RunExternally.gd.uid | 1 + addons/gut/gui/RunExternally.tscn | 65 + addons/gut/gui/RunResults.gd | 250 ++ addons/gut/gui/RunResults.gd.uid | 1 + addons/gut/gui/RunResults.tscn | 109 + addons/gut/gui/Settings.tscn | 7 + addons/gut/gui/ShellOutOptions.gd | 321 ++ addons/gut/gui/ShellOutOptions.gd.uid | 1 + addons/gut/gui/ShellOutOptions.tscn | 34 + addons/gut/gui/ShortcutButton.gd | 154 + addons/gut/gui/ShortcutButton.gd.uid | 1 + addons/gut/gui/ShortcutButton.tscn | 55 + addons/gut/gui/ShortcutDialog.gd | 120 + addons/gut/gui/ShortcutDialog.gd.uid | 1 + addons/gut/gui/ShortcutDialog.tscn | 209 ++ addons/gut/gui/about.gd | 136 + addons/gut/gui/about.gd.uid | 1 + addons/gut/gui/about.tscn | 68 + addons/gut/gui/arrow.png | Bin 0 -> 122 bytes addons/gut/gui/arrow.png.import | 40 + addons/gut/gui/check_for_update.gd | 152 + addons/gut/gui/check_for_update.gd.uid | 1 + addons/gut/gui/check_for_update.tscn | 45 + addons/gut/gui/editor_globals.gd | 68 + addons/gut/gui/editor_globals.gd.uid | 1 + addons/gut/gui/gut_config_gui.gd | 282 ++ addons/gut/gui/gut_config_gui.gd.uid | 1 + addons/gut/gui/gut_dock.gd | 37 + addons/gut/gui/gut_dock.gd.uid | 1 + addons/gut/gui/gut_gui.gd | 239 ++ addons/gut/gui/gut_gui.gd.uid | 1 + addons/gut/gui/gut_logo.gd | 225 ++ addons/gut/gui/gut_logo.gd.uid | 1 + addons/gut/gui/gut_user_preferences.gd | 79 + addons/gut/gui/gut_user_preferences.gd.uid | 1 + addons/gut/gui/option_maker.gd | 124 + addons/gut/gui/option_maker.gd.uid | 1 + addons/gut/gui/panel_controls.gd | 484 +++ addons/gut/gui/panel_controls.gd.uid | 1 + addons/gut/gui/play.png | Bin 0 -> 143 bytes addons/gut/gui/play.png.import | 40 + addons/gut/gui/run_from_editor.gd | 22 + addons/gut/gui/run_from_editor.gd.uid | 1 + addons/gut/gui/run_from_editor.tscn | 6 + addons/gut/gui/update_required.gd | 30 + addons/gut/gui/update_required.gd.uid | 1 + addons/gut/gui/update_required.tscn | 17 + addons/gut/gut.gd | 1278 +++++++ addons/gut/gut.gd.uid | 1 + addons/gut/gut_cmdln.gd | 71 + addons/gut/gut_cmdln.gd.uid | 1 + addons/gut/gut_config.gd | 236 ++ addons/gut/gut_config.gd.uid | 1 + addons/gut/gut_constants.gd | 130 + addons/gut/gut_constants.gd.uid | 1 + addons/gut/gut_fonts.gd | 94 + addons/gut/gut_fonts.gd.uid | 1 + addons/gut/gut_loader.gd | 75 + addons/gut/gut_loader.gd.uid | 1 + addons/gut/gut_loader_the_scene.tscn | 7 + addons/gut/gut_menu.gd | 104 + addons/gut/gut_menu.gd.uid | 1 + addons/gut/gut_plugin.gd | 157 + addons/gut/gut_plugin.gd.uid | 1 + addons/gut/gut_to_move.gd | 92 + addons/gut/gut_to_move.gd.uid | 1 + addons/gut/gut_tracked_error.gd | 95 + addons/gut/gut_tracked_error.gd.uid | 1 + addons/gut/gut_vscode_debugger.gd | 43 + addons/gut/gut_vscode_debugger.gd.uid | 1 + addons/gut/hook_script.gd | 57 + addons/gut/hook_script.gd.uid | 1 + addons/gut/icon.png | Bin 0 -> 129 bytes addons/gut/icon.png.import | 40 + addons/gut/images/Folder.svg | 1 + addons/gut/images/Folder.svg.import | 43 + addons/gut/images/GutIconV2_base.png | Bin 0 -> 135150 bytes addons/gut/images/GutIconV2_base.png.import | 40 + addons/gut/images/GutIconV2_no_shine.png | Bin 0 -> 48913 bytes .../gut/images/GutIconV2_no_shine.png.import | 40 + addons/gut/images/HSplitContainer.svg | 39 + addons/gut/images/HSplitContainer.svg.import | 44 + addons/gut/images/Script.svg | 1 + addons/gut/images/Script.svg.import | 43 + addons/gut/images/VSplitContainer.svg | 39 + addons/gut/images/VSplitContainer.svg.import | 44 + addons/gut/images/eyey.png | Bin 0 -> 3721 bytes addons/gut/images/eyey.png.import | 40 + addons/gut/images/green.png | Bin 0 -> 213 bytes addons/gut/images/green.png.import | 40 + addons/gut/images/red.png | Bin 0 -> 213 bytes addons/gut/images/red.png.import | 40 + addons/gut/images/yellow.png | Bin 0 -> 213 bytes addons/gut/images/yellow.png.import | 40 + addons/gut/inner_class_registry.gd | 73 + addons/gut/inner_class_registry.gd.uid | 1 + addons/gut/input_factory.gd | 159 + addons/gut/input_factory.gd.uid | 1 + addons/gut/input_sender.gd | 651 ++++ addons/gut/input_sender.gd.uid | 1 + addons/gut/junit_xml_export.gd | 111 + addons/gut/junit_xml_export.gd.uid | 1 + addons/gut/lazy_loader.gd | 39 + addons/gut/lazy_loader.gd.uid | 1 + addons/gut/logger.gd | 431 +++ addons/gut/logger.gd.uid | 1 + addons/gut/menu_manager.gd.uid | 1 + addons/gut/method_maker.gd | 192 ++ addons/gut/method_maker.gd.uid | 1 + addons/gut/one_to_many.gd | 46 + addons/gut/one_to_many.gd.uid | 1 + addons/gut/orphan_counter.gd | 221 ++ addons/gut/orphan_counter.gd.uid | 1 + addons/gut/parameter_factory.gd | 91 + addons/gut/parameter_factory.gd.uid | 1 + addons/gut/parameter_handler.gd | 36 + addons/gut/parameter_handler.gd.uid | 1 + addons/gut/plugin.cfg | 7 + addons/gut/printers.gd | 184 ++ addons/gut/printers.gd.uid | 1 + addons/gut/result_exporter.gd | 125 + addons/gut/result_exporter.gd.uid | 1 + addons/gut/script_parser.gd | 339 ++ addons/gut/script_parser.gd.uid | 1 + addons/gut/signal_watcher.gd | 218 ++ addons/gut/signal_watcher.gd.uid | 1 + addons/gut/singleton_parser.gd | 88 + addons/gut/singleton_parser.gd.uid | 1 + addons/gut/source_code_pro.fnt | Bin 0 -> 42799 bytes addons/gut/source_code_pro.fnt.import | 17 + addons/gut/spy.gd | 116 + addons/gut/spy.gd.uid | 1 + addons/gut/strutils.gd | 175 + addons/gut/strutils.gd.uid | 1 + addons/gut/stub_params.gd | 240 ++ addons/gut/stub_params.gd.uid | 1 + addons/gut/stubber.gd | 204 ++ addons/gut/stubber.gd.uid | 1 + addons/gut/stubs.gd | 110 + addons/gut/stubs.gd.uid | 1 + addons/gut/summary.gd | 220 ++ addons/gut/summary.gd.uid | 1 + addons/gut/test.gd | 2923 +++++++++++++++++ addons/gut/test.gd.uid | 1 + addons/gut/test_collector.gd | 277 ++ addons/gut/test_collector.gd.uid | 1 + addons/gut/thing_counter.gd | 55 + addons/gut/thing_counter.gd.uid | 1 + addons/gut/update_detector.gd | 346 ++ addons/gut/update_detector.gd.uid | 1 + addons/gut/utils.gd | 672 ++++ addons/gut/utils.gd.uid | 1 + addons/gut/version_conversion.gd | 120 + addons/gut/version_conversion.gd.uid | 1 + addons/gut/version_numbers.gd | 127 + addons/gut/version_numbers.gd.uid | 1 + addons/gut/versions.json | 39 + addons/gut/warnings_manager.gd | 176 + addons/gut/warnings_manager.gd.uid | 1 + docs/ARCHITECTURE.md | 103 + docs/NETCODE.md | 156 + docs/ROADMAP.md | 56 + docs/WORKFLOW.md | 177 + icon.svg | 10 + icon.svg.import | 43 + project.godot | 88 + resources/bosses/warden.tres | 157 + resources/enemies/drifter.tres | 23 + resources/enemies/dummy.tres | 14 + resources/enemies/stalker.tres | 16 + resources/enemies/turret.tres | 25 + src/actors/boss/boss_def.gd | 24 + src/actors/boss/boss_def.gd.uid | 1 + src/actors/boss/boss_phase.gd | 18 + src/actors/boss/boss_phase.gd.uid | 1 + src/actors/enemies/enemy_def.gd | 36 + src/actors/enemies/enemy_def.gd.uid | 1 + src/autoload/net.gd | 249 ++ src/autoload/net.gd.uid | 1 + src/content/content.gd | 299 ++ src/content/content.gd.uid | 1 + src/core/game_log.gd | 37 + src/core/game_log.gd.uid | 1 + src/core/game_opts.gd | 67 + src/core/game_opts.gd.uid | 1 + src/core/movement.gd | 31 + src/core/movement.gd.uid | 1 + src/core/sim_config.gd | 64 + src/core/sim_config.gd.uid | 1 + src/game.tscn | 19 + src/instances/instance.gd | 147 + src/instances/instance.gd.uid | 1 + src/main.gd | 112 + src/main.gd.uid | 1 + src/main.tscn | 6 + src/net/client_runtime.gd | 252 ++ src/net/client_runtime.gd.uid | 1 + src/net/net_codec.gd | 234 ++ src/net/net_codec.gd.uid | 1 + src/net/protocol.gd | 24 + src/net/protocol.gd.uid | 1 + src/net/server_runtime.gd | 193 ++ src/net/server_runtime.gd.uid | 1 + src/sim/bullet_pool.gd | 161 + src/sim/bullet_pool.gd.uid | 1 + src/sim/input_frame.gd | 48 + src/sim/input_frame.gd.uid | 1 + src/sim/patterns/aimed_spread_emitter.gd | 27 + src/sim/patterns/aimed_spread_emitter.gd.uid | 1 + src/sim/patterns/arc_sweep_emitter.gd | 31 + src/sim/patterns/arc_sweep_emitter.gd.uid | 1 + src/sim/patterns/bullet_emitter.gd | 60 + src/sim/patterns/bullet_emitter.gd.uid | 1 + src/sim/patterns/emit_context.gd | 23 + src/sim/patterns/emit_context.gd.uid | 1 + src/sim/patterns/ring_emitter.gd | 29 + src/sim/patterns/ring_emitter.gd.uid | 1 + src/sim/patterns/wall_gap_emitter.gd | 47 + src/sim/patterns/wall_gap_emitter.gd.uid | 1 + src/sim/sim_boss.gd | 25 + src/sim/sim_boss.gd.uid | 1 + src/sim/sim_enemy.gd | 23 + src/sim/sim_enemy.gd.uid | 1 + src/sim/sim_events.gd | 22 + src/sim/sim_events.gd.uid | 1 + src/sim/sim_player.gd | 41 + src/sim/sim_player.gd.uid | 1 + src/sim/sim_world.gd | 430 +++ src/sim/sim_world.gd.uid | 1 + src/ui/hud.gd | 114 + src/ui/hud.gd.uid | 1 + src/ui/main_menu.gd | 80 + src/ui/main_menu.gd.uid | 1 + src/view/bullet_renderer.gd | 60 + src/view/bullet_renderer.gd.uid | 1 + src/view/game_scene.gd | 29 + src/view/game_scene.gd.uid | 1 + src/view/world_view.gd | 119 + src/view/world_view.gd.uid | 1 + tests/integration/test_dungeon_flow.gd | 109 + tests/integration/test_dungeon_flow.gd.uid | 1 + tests/integration/test_replica_parity.gd | 127 + tests/integration/test_replica_parity.gd.uid | 1 + tests/unit/test_boss.gd | 115 + tests/unit/test_boss.gd.uid | 1 + tests/unit/test_bullet_pool.gd | 94 + tests/unit/test_bullet_pool.gd.uid | 1 + tests/unit/test_emergency_escape.gd | 70 + tests/unit/test_emergency_escape.gd.uid | 1 + tests/unit/test_emitters.gd | 147 + tests/unit/test_emitters.gd.uid | 1 + tests/unit/test_input_frame.gd | 43 + tests/unit/test_input_frame.gd.uid | 1 + tests/unit/test_movement.gd | 39 + tests/unit/test_movement.gd.uid | 1 + tests/unit/test_net_codec.gd | 107 + tests/unit/test_net_codec.gd.uid | 1 + tests/unit/test_server_authority.gd | 138 + tests/unit/test_server_authority.gd.uid | 1 + tools/bench.gd | 83 + tools/bench.gd.uid | 1 + tools/check.sh | 35 + tools/check.tscn | 6 + tools/check_scripts.gd | 48 + tools/check_scripts.gd.uid | 1 + tools/client.sh | 9 + tools/export_content.gd | 36 + tools/export_content.gd.uid | 1 + tools/install-hooks.sh | 8 + tools/server.sh | 8 + tools/setup_input_map.gd | 48 + tools/setup_input_map.gd.uid | 1 + tools/smoke.sh | 91 + tools/test.sh | 19 + 385 files changed, 28725 insertions(+) create mode 100644 .claude/commands/newboss.md create mode 100644 .claude/commands/playtest.md create mode 100644 .claude/commands/verify.md create mode 100644 .claude/settings.json create mode 100644 .gitattributes create mode 100755 .githooks/pre-commit create mode 100755 .githooks/pre-push create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 addons/gut/GutScene.gd create mode 100644 addons/gut/GutScene.gd.uid create mode 100644 addons/gut/GutScene.tscn create mode 100644 addons/gut/LICENSE.md create mode 100644 addons/gut/UserFileViewer.gd create mode 100644 addons/gut/UserFileViewer.gd.uid create mode 100644 addons/gut/UserFileViewer.tscn create mode 100644 addons/gut/autofree.gd create mode 100644 addons/gut/autofree.gd.uid create mode 100644 addons/gut/awaiter.gd create mode 100644 addons/gut/awaiter.gd.uid create mode 100644 addons/gut/cli/change_project_warnings.gd create mode 100644 addons/gut/cli/change_project_warnings.gd.uid create mode 100644 addons/gut/cli/gut_cli.gd create mode 100644 addons/gut/cli/gut_cli.gd.uid create mode 100644 addons/gut/cli/optparse.gd create mode 100644 addons/gut/cli/optparse.gd.uid create mode 100644 addons/gut/collected_script.gd create mode 100644 addons/gut/collected_script.gd.uid create mode 100644 addons/gut/collected_test.gd create mode 100644 addons/gut/collected_test.gd.uid create mode 100644 addons/gut/comparator.gd create mode 100644 addons/gut/comparator.gd.uid create mode 100644 addons/gut/compare_result.gd create mode 100644 addons/gut/compare_result.gd.uid create mode 100644 addons/gut/diff_formatter.gd create mode 100644 addons/gut/diff_formatter.gd.uid create mode 100644 addons/gut/diff_tool.gd create mode 100644 addons/gut/diff_tool.gd.uid create mode 100644 addons/gut/double_templates/double_data_template.txt create mode 100644 addons/gut/double_templates/function_template.txt create mode 100644 addons/gut/double_templates/init_template.txt create mode 100644 addons/gut/double_templates/script_template.txt create mode 100644 addons/gut/double_templates/singleton_template.txt create mode 100644 addons/gut/double_tools.gd create mode 100644 addons/gut/double_tools.gd.uid create mode 100644 addons/gut/doubler.gd create mode 100644 addons/gut/doubler.gd.uid create mode 100644 addons/gut/dynamic_gdscript.gd create mode 100644 addons/gut/dynamic_gdscript.gd.uid create mode 100644 addons/gut/editor_caret_context_notifier.gd create mode 100644 addons/gut/editor_caret_context_notifier.gd.uid create mode 100644 addons/gut/error_tracker.gd create mode 100644 addons/gut/error_tracker.gd.uid create mode 100644 addons/gut/fonts/AnonymousPro-Bold.ttf create mode 100644 addons/gut/fonts/AnonymousPro-Bold.ttf.import create mode 100644 addons/gut/fonts/AnonymousPro-BoldItalic.ttf create mode 100644 addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import create mode 100644 addons/gut/fonts/AnonymousPro-Italic.ttf create mode 100644 addons/gut/fonts/AnonymousPro-Italic.ttf.import create mode 100644 addons/gut/fonts/AnonymousPro-Regular.ttf create mode 100644 addons/gut/fonts/AnonymousPro-Regular.ttf.import create mode 100644 addons/gut/fonts/CourierPrime-Bold.ttf create mode 100644 addons/gut/fonts/CourierPrime-Bold.ttf.import create mode 100644 addons/gut/fonts/CourierPrime-BoldItalic.ttf create mode 100644 addons/gut/fonts/CourierPrime-BoldItalic.ttf.import create mode 100644 addons/gut/fonts/CourierPrime-Italic.ttf create mode 100644 addons/gut/fonts/CourierPrime-Italic.ttf.import create mode 100644 addons/gut/fonts/CourierPrime-Regular.ttf create mode 100644 addons/gut/fonts/CourierPrime-Regular.ttf.import create mode 100644 addons/gut/fonts/LobsterTwo-Bold.ttf create mode 100644 addons/gut/fonts/LobsterTwo-Bold.ttf.import create mode 100644 addons/gut/fonts/LobsterTwo-BoldItalic.ttf create mode 100644 addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import create mode 100644 addons/gut/fonts/LobsterTwo-Italic.ttf create mode 100644 addons/gut/fonts/LobsterTwo-Italic.ttf.import create mode 100644 addons/gut/fonts/LobsterTwo-Regular.ttf create mode 100644 addons/gut/fonts/LobsterTwo-Regular.ttf.import create mode 100644 addons/gut/fonts/OFL.txt create mode 100644 addons/gut/get_editor_interface.gd create mode 100644 addons/gut/get_editor_interface.gd.uid create mode 100644 addons/gut/godot_singletons.gd create mode 100644 addons/gut/godot_singletons.gd.uid create mode 100644 addons/gut/gui/EditorRadioButton.tres create mode 100644 addons/gut/gui/GutBottomPanel.gd create mode 100644 addons/gut/gui/GutBottomPanel.gd.uid create mode 100644 addons/gut/gui/GutBottomPanel.tscn create mode 100644 addons/gut/gui/GutControl.gd create mode 100644 addons/gut/gui/GutControl.gd.uid create mode 100644 addons/gut/gui/GutControl.tscn create mode 100644 addons/gut/gui/GutLogo.tscn create mode 100644 addons/gut/gui/GutRunner.gd create mode 100644 addons/gut/gui/GutRunner.gd.uid create mode 100644 addons/gut/gui/GutRunner.tscn create mode 100644 addons/gut/gui/GutSceneTheme.tres create mode 100644 addons/gut/gui/MinGui.tscn create mode 100644 addons/gut/gui/NormalGui.tscn create mode 100644 addons/gut/gui/OutputText.gd create mode 100644 addons/gut/gui/OutputText.gd.uid create mode 100644 addons/gut/gui/OutputText.tscn create mode 100644 addons/gut/gui/ResizeHandle.gd create mode 100644 addons/gut/gui/ResizeHandle.gd.uid create mode 100644 addons/gut/gui/ResizeHandle.tscn create mode 100644 addons/gut/gui/ResultsTree.gd create mode 100644 addons/gut/gui/ResultsTree.gd.uid create mode 100644 addons/gut/gui/ResultsTree.tscn create mode 100644 addons/gut/gui/RunAtCursor.gd create mode 100644 addons/gut/gui/RunAtCursor.gd.uid create mode 100644 addons/gut/gui/RunAtCursor.tscn create mode 100644 addons/gut/gui/RunExternally.gd create mode 100644 addons/gut/gui/RunExternally.gd.uid create mode 100644 addons/gut/gui/RunExternally.tscn create mode 100644 addons/gut/gui/RunResults.gd create mode 100644 addons/gut/gui/RunResults.gd.uid create mode 100644 addons/gut/gui/RunResults.tscn create mode 100644 addons/gut/gui/Settings.tscn create mode 100644 addons/gut/gui/ShellOutOptions.gd create mode 100644 addons/gut/gui/ShellOutOptions.gd.uid create mode 100644 addons/gut/gui/ShellOutOptions.tscn create mode 100644 addons/gut/gui/ShortcutButton.gd create mode 100644 addons/gut/gui/ShortcutButton.gd.uid create mode 100644 addons/gut/gui/ShortcutButton.tscn create mode 100644 addons/gut/gui/ShortcutDialog.gd create mode 100644 addons/gut/gui/ShortcutDialog.gd.uid create mode 100644 addons/gut/gui/ShortcutDialog.tscn create mode 100644 addons/gut/gui/about.gd create mode 100644 addons/gut/gui/about.gd.uid create mode 100644 addons/gut/gui/about.tscn create mode 100644 addons/gut/gui/arrow.png create mode 100644 addons/gut/gui/arrow.png.import create mode 100644 addons/gut/gui/check_for_update.gd create mode 100644 addons/gut/gui/check_for_update.gd.uid create mode 100644 addons/gut/gui/check_for_update.tscn create mode 100644 addons/gut/gui/editor_globals.gd create mode 100644 addons/gut/gui/editor_globals.gd.uid create mode 100644 addons/gut/gui/gut_config_gui.gd create mode 100644 addons/gut/gui/gut_config_gui.gd.uid create mode 100644 addons/gut/gui/gut_dock.gd create mode 100644 addons/gut/gui/gut_dock.gd.uid create mode 100644 addons/gut/gui/gut_gui.gd create mode 100644 addons/gut/gui/gut_gui.gd.uid create mode 100644 addons/gut/gui/gut_logo.gd create mode 100644 addons/gut/gui/gut_logo.gd.uid create mode 100644 addons/gut/gui/gut_user_preferences.gd create mode 100644 addons/gut/gui/gut_user_preferences.gd.uid create mode 100644 addons/gut/gui/option_maker.gd create mode 100644 addons/gut/gui/option_maker.gd.uid create mode 100644 addons/gut/gui/panel_controls.gd create mode 100644 addons/gut/gui/panel_controls.gd.uid create mode 100644 addons/gut/gui/play.png create mode 100644 addons/gut/gui/play.png.import create mode 100644 addons/gut/gui/run_from_editor.gd create mode 100644 addons/gut/gui/run_from_editor.gd.uid create mode 100644 addons/gut/gui/run_from_editor.tscn create mode 100644 addons/gut/gui/update_required.gd create mode 100644 addons/gut/gui/update_required.gd.uid create mode 100644 addons/gut/gui/update_required.tscn create mode 100644 addons/gut/gut.gd create mode 100644 addons/gut/gut.gd.uid create mode 100644 addons/gut/gut_cmdln.gd create mode 100644 addons/gut/gut_cmdln.gd.uid create mode 100644 addons/gut/gut_config.gd create mode 100644 addons/gut/gut_config.gd.uid create mode 100644 addons/gut/gut_constants.gd create mode 100644 addons/gut/gut_constants.gd.uid create mode 100644 addons/gut/gut_fonts.gd create mode 100644 addons/gut/gut_fonts.gd.uid create mode 100644 addons/gut/gut_loader.gd create mode 100644 addons/gut/gut_loader.gd.uid create mode 100644 addons/gut/gut_loader_the_scene.tscn create mode 100644 addons/gut/gut_menu.gd create mode 100644 addons/gut/gut_menu.gd.uid create mode 100644 addons/gut/gut_plugin.gd create mode 100644 addons/gut/gut_plugin.gd.uid create mode 100644 addons/gut/gut_to_move.gd create mode 100644 addons/gut/gut_to_move.gd.uid create mode 100644 addons/gut/gut_tracked_error.gd create mode 100644 addons/gut/gut_tracked_error.gd.uid create mode 100644 addons/gut/gut_vscode_debugger.gd create mode 100644 addons/gut/gut_vscode_debugger.gd.uid create mode 100644 addons/gut/hook_script.gd create mode 100644 addons/gut/hook_script.gd.uid create mode 100644 addons/gut/icon.png create mode 100644 addons/gut/icon.png.import create mode 100644 addons/gut/images/Folder.svg create mode 100644 addons/gut/images/Folder.svg.import create mode 100644 addons/gut/images/GutIconV2_base.png create mode 100644 addons/gut/images/GutIconV2_base.png.import create mode 100644 addons/gut/images/GutIconV2_no_shine.png create mode 100644 addons/gut/images/GutIconV2_no_shine.png.import create mode 100644 addons/gut/images/HSplitContainer.svg create mode 100644 addons/gut/images/HSplitContainer.svg.import create mode 100644 addons/gut/images/Script.svg create mode 100644 addons/gut/images/Script.svg.import create mode 100644 addons/gut/images/VSplitContainer.svg create mode 100644 addons/gut/images/VSplitContainer.svg.import create mode 100644 addons/gut/images/eyey.png create mode 100644 addons/gut/images/eyey.png.import create mode 100644 addons/gut/images/green.png create mode 100644 addons/gut/images/green.png.import create mode 100644 addons/gut/images/red.png create mode 100644 addons/gut/images/red.png.import create mode 100644 addons/gut/images/yellow.png create mode 100644 addons/gut/images/yellow.png.import create mode 100644 addons/gut/inner_class_registry.gd create mode 100644 addons/gut/inner_class_registry.gd.uid create mode 100644 addons/gut/input_factory.gd create mode 100644 addons/gut/input_factory.gd.uid create mode 100644 addons/gut/input_sender.gd create mode 100644 addons/gut/input_sender.gd.uid create mode 100644 addons/gut/junit_xml_export.gd create mode 100644 addons/gut/junit_xml_export.gd.uid create mode 100644 addons/gut/lazy_loader.gd create mode 100644 addons/gut/lazy_loader.gd.uid create mode 100644 addons/gut/logger.gd create mode 100644 addons/gut/logger.gd.uid create mode 100644 addons/gut/menu_manager.gd.uid create mode 100644 addons/gut/method_maker.gd create mode 100644 addons/gut/method_maker.gd.uid create mode 100644 addons/gut/one_to_many.gd create mode 100644 addons/gut/one_to_many.gd.uid create mode 100644 addons/gut/orphan_counter.gd create mode 100644 addons/gut/orphan_counter.gd.uid create mode 100644 addons/gut/parameter_factory.gd create mode 100644 addons/gut/parameter_factory.gd.uid create mode 100644 addons/gut/parameter_handler.gd create mode 100644 addons/gut/parameter_handler.gd.uid create mode 100644 addons/gut/plugin.cfg create mode 100644 addons/gut/printers.gd create mode 100644 addons/gut/printers.gd.uid create mode 100644 addons/gut/result_exporter.gd create mode 100644 addons/gut/result_exporter.gd.uid create mode 100644 addons/gut/script_parser.gd create mode 100644 addons/gut/script_parser.gd.uid create mode 100644 addons/gut/signal_watcher.gd create mode 100644 addons/gut/signal_watcher.gd.uid create mode 100644 addons/gut/singleton_parser.gd create mode 100644 addons/gut/singleton_parser.gd.uid create mode 100644 addons/gut/source_code_pro.fnt create mode 100644 addons/gut/source_code_pro.fnt.import create mode 100644 addons/gut/spy.gd create mode 100644 addons/gut/spy.gd.uid create mode 100644 addons/gut/strutils.gd create mode 100644 addons/gut/strutils.gd.uid create mode 100644 addons/gut/stub_params.gd create mode 100644 addons/gut/stub_params.gd.uid create mode 100644 addons/gut/stubber.gd create mode 100644 addons/gut/stubber.gd.uid create mode 100644 addons/gut/stubs.gd create mode 100644 addons/gut/stubs.gd.uid create mode 100644 addons/gut/summary.gd create mode 100644 addons/gut/summary.gd.uid create mode 100644 addons/gut/test.gd create mode 100644 addons/gut/test.gd.uid create mode 100644 addons/gut/test_collector.gd create mode 100644 addons/gut/test_collector.gd.uid create mode 100644 addons/gut/thing_counter.gd create mode 100644 addons/gut/thing_counter.gd.uid create mode 100644 addons/gut/update_detector.gd create mode 100644 addons/gut/update_detector.gd.uid create mode 100644 addons/gut/utils.gd create mode 100644 addons/gut/utils.gd.uid create mode 100644 addons/gut/version_conversion.gd create mode 100644 addons/gut/version_conversion.gd.uid create mode 100644 addons/gut/version_numbers.gd create mode 100644 addons/gut/version_numbers.gd.uid create mode 100644 addons/gut/versions.json create mode 100644 addons/gut/warnings_manager.gd create mode 100644 addons/gut/warnings_manager.gd.uid create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/NETCODE.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/WORKFLOW.md create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 project.godot create mode 100644 resources/bosses/warden.tres create mode 100644 resources/enemies/drifter.tres create mode 100644 resources/enemies/dummy.tres create mode 100644 resources/enemies/stalker.tres create mode 100644 resources/enemies/turret.tres create mode 100644 src/actors/boss/boss_def.gd create mode 100644 src/actors/boss/boss_def.gd.uid create mode 100644 src/actors/boss/boss_phase.gd create mode 100644 src/actors/boss/boss_phase.gd.uid create mode 100644 src/actors/enemies/enemy_def.gd create mode 100644 src/actors/enemies/enemy_def.gd.uid create mode 100644 src/autoload/net.gd create mode 100644 src/autoload/net.gd.uid create mode 100644 src/content/content.gd create mode 100644 src/content/content.gd.uid create mode 100644 src/core/game_log.gd create mode 100644 src/core/game_log.gd.uid create mode 100644 src/core/game_opts.gd create mode 100644 src/core/game_opts.gd.uid create mode 100644 src/core/movement.gd create mode 100644 src/core/movement.gd.uid create mode 100644 src/core/sim_config.gd create mode 100644 src/core/sim_config.gd.uid create mode 100644 src/game.tscn create mode 100644 src/instances/instance.gd create mode 100644 src/instances/instance.gd.uid create mode 100644 src/main.gd create mode 100644 src/main.gd.uid create mode 100644 src/main.tscn create mode 100644 src/net/client_runtime.gd create mode 100644 src/net/client_runtime.gd.uid create mode 100644 src/net/net_codec.gd create mode 100644 src/net/net_codec.gd.uid create mode 100644 src/net/protocol.gd create mode 100644 src/net/protocol.gd.uid create mode 100644 src/net/server_runtime.gd create mode 100644 src/net/server_runtime.gd.uid create mode 100644 src/sim/bullet_pool.gd create mode 100644 src/sim/bullet_pool.gd.uid create mode 100644 src/sim/input_frame.gd create mode 100644 src/sim/input_frame.gd.uid create mode 100644 src/sim/patterns/aimed_spread_emitter.gd create mode 100644 src/sim/patterns/aimed_spread_emitter.gd.uid create mode 100644 src/sim/patterns/arc_sweep_emitter.gd create mode 100644 src/sim/patterns/arc_sweep_emitter.gd.uid create mode 100644 src/sim/patterns/bullet_emitter.gd create mode 100644 src/sim/patterns/bullet_emitter.gd.uid create mode 100644 src/sim/patterns/emit_context.gd create mode 100644 src/sim/patterns/emit_context.gd.uid create mode 100644 src/sim/patterns/ring_emitter.gd create mode 100644 src/sim/patterns/ring_emitter.gd.uid create mode 100644 src/sim/patterns/wall_gap_emitter.gd create mode 100644 src/sim/patterns/wall_gap_emitter.gd.uid create mode 100644 src/sim/sim_boss.gd create mode 100644 src/sim/sim_boss.gd.uid create mode 100644 src/sim/sim_enemy.gd create mode 100644 src/sim/sim_enemy.gd.uid create mode 100644 src/sim/sim_events.gd create mode 100644 src/sim/sim_events.gd.uid create mode 100644 src/sim/sim_player.gd create mode 100644 src/sim/sim_player.gd.uid create mode 100644 src/sim/sim_world.gd create mode 100644 src/sim/sim_world.gd.uid create mode 100644 src/ui/hud.gd create mode 100644 src/ui/hud.gd.uid create mode 100644 src/ui/main_menu.gd create mode 100644 src/ui/main_menu.gd.uid create mode 100644 src/view/bullet_renderer.gd create mode 100644 src/view/bullet_renderer.gd.uid create mode 100644 src/view/game_scene.gd create mode 100644 src/view/game_scene.gd.uid create mode 100644 src/view/world_view.gd create mode 100644 src/view/world_view.gd.uid create mode 100644 tests/integration/test_dungeon_flow.gd create mode 100644 tests/integration/test_dungeon_flow.gd.uid create mode 100644 tests/integration/test_replica_parity.gd create mode 100644 tests/integration/test_replica_parity.gd.uid create mode 100644 tests/unit/test_boss.gd create mode 100644 tests/unit/test_boss.gd.uid create mode 100644 tests/unit/test_bullet_pool.gd create mode 100644 tests/unit/test_bullet_pool.gd.uid create mode 100644 tests/unit/test_emergency_escape.gd create mode 100644 tests/unit/test_emergency_escape.gd.uid create mode 100644 tests/unit/test_emitters.gd create mode 100644 tests/unit/test_emitters.gd.uid create mode 100644 tests/unit/test_input_frame.gd create mode 100644 tests/unit/test_input_frame.gd.uid create mode 100644 tests/unit/test_movement.gd create mode 100644 tests/unit/test_movement.gd.uid create mode 100644 tests/unit/test_net_codec.gd create mode 100644 tests/unit/test_net_codec.gd.uid create mode 100644 tests/unit/test_server_authority.gd create mode 100644 tests/unit/test_server_authority.gd.uid create mode 100644 tools/bench.gd create mode 100644 tools/bench.gd.uid create mode 100755 tools/check.sh create mode 100644 tools/check.tscn create mode 100644 tools/check_scripts.gd create mode 100644 tools/check_scripts.gd.uid create mode 100755 tools/client.sh create mode 100644 tools/export_content.gd create mode 100644 tools/export_content.gd.uid create mode 100755 tools/install-hooks.sh create mode 100755 tools/server.sh create mode 100644 tools/setup_input_map.gd create mode 100644 tools/setup_input_map.gd.uid create mode 100755 tools/smoke.sh create mode 100755 tools/test.sh diff --git a/.claude/commands/newboss.md b/.claude/commands/newboss.md new file mode 100644 index 0000000..9dc7033 --- /dev/null +++ b/.claude/commands/newboss.md @@ -0,0 +1,22 @@ +--- +description: Add a new boss as data, following the existing format +argument-hint: +--- + +Add a boss: $ARGUMENTS + +Do it as data, not code: + +1. Read `src/content/content.gd` — the Warden is the reference. Its phases layer + one idea at a time, which is the design intent, not an accident. +2. Add a `static func` returning a `BossDef`, built from the emitters in + `src/sim/patterns/`. Register the id in `Content.boss()` and add a + `BOSS_*` constant. +3. Give every phase a `telegraph_ticks` window so the transition is readable. +4. Add tests to `tests/unit/test_boss.gd`: phase thresholds, and that the + pattern actually fills the arena. +5. Run `tools/check.sh` then `tools/test.sh`. + +If you find yourself wanting to add a branch to `SimWorld` for this boss, stop +and add a new `BulletEmitter` subclass instead — the format is supposed to +absorb the difference. diff --git a/.claude/commands/playtest.md b/.claude/commands/playtest.md new file mode 100644 index 0000000..3fdf9c7 --- /dev/null +++ b/.claude/commands/playtest.md @@ -0,0 +1,14 @@ +--- +description: Launch a windowed client against a fresh local dedicated server +--- + +Start a dedicated server in the background and a windowed client against it, so +the change can be looked at rather than inferred: + +```bash +tools/server.sh --port 27015 & +tools/client.sh --join --host 127.0.0.1 --port 27015 --name dev +``` + +Remember to stop the background server afterwards. If there is no display +available, use `tools/smoke.sh` instead and say that is what you did. diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 0000000..7743515 --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,15 @@ +--- +description: Run the full verification chain (parse check, tests, end-to-end smoke) +--- + +Run all three, in order, and stop at the first failure: + +1. `tools/check.sh` — parse-checks every script +2. `tools/test.sh` — GUT suite +3. `tools/smoke.sh` — real server plus two bot clients over ENet + +Report the outcome of each plainly. If the smoke test fails, its output includes +the tails of the server and client logs — read them before guessing. + +Skip step 3 only if the change touched nothing under `src/net/`, `src/sim/`, +`src/instances/` or `src/autoload/`, and say that you skipped it and why. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d22d9c3 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(tools/check.sh)", + "Bash(./tools/check.sh)", + "Bash(tools/test.sh:*)", + "Bash(./tools/test.sh:*)", + "Bash(tools/smoke.sh)", + "Bash(./tools/smoke.sh)", + "Bash(godot --headless --path . --import)", + "Bash(godot --headless --path . --script tools/bench.gd:*)", + "Bash(godot --headless --path . --script tools/export_content.gd)", + "Bash(godot --headless --path . res://tools/check.tscn)", + "Bash(godot --version)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0abc8df --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Godot text resources: keep diffs readable and line endings stable. +*.gd text eol=lf +*.tscn text eol=lf +*.tres text eol=lf +*.godot text eol=lf diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..f3be2e3 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Fast, local sanity checks before a commit is allowed to happen. +# +# Deliberately dependency-free (plain bash + git + godot), matching the rest +# of tools/*.sh -- no pip install, no pre-commit framework, so `tools/install-hooks.sh` +# is the entire setup cost for a fresh clone. +# +# Scope: things that are cheap enough to run on every commit (a few seconds). +# The full end-to-end smoke test lives in pre-push instead, where 35s is a +# reasonable price to pay less often. +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" + +fail=0 +staged=$(git diff --cached --name-only --diff-filter=ACMR) + +if [[ -z "$staged" ]]; then + exit 0 +fi + +echo "pre-commit checks..." + +# --- Hygiene, on every commit, regardless of file type ----------------- + +if git diff --cached --check --diff-filter=ACMR | grep -q .; then + echo "FAIL: unresolved merge conflict markers" + git diff --cached --check --diff-filter=ACMR + fail=1 +fi + +big=$(git diff --cached --diff-filter=ACMR --name-only -z | \ + xargs -0 -I{} du -k "{}" 2>/dev/null | awk '$1 > 5120 {print}') +if [[ -n "$big" ]]; then + echo "FAIL: file(s) over 5MB staged -- game assets belong outside git history," + echo " or need Git LFS if they truly must be tracked:" + echo "$big" | sed 's/^/ /' + fail=1 +fi + +if grep -qE '^\.godot/$' <(git diff --cached --name-only) 2>/dev/null || \ + git diff --cached --name-only | grep -qE '^\.godot/'; then + echo "FAIL: .godot/ is generated (import cache + class cache) and must stay gitignored" + fail=1 +fi + +# --- GDScript-specific, only when a .gd file is staged ------------------ + +# addons/ is vendored (GUT) -- not ours to hold to our own style or to parse- +# check on every commit; it never changes except on a deliberate upgrade. +gd_files=$(echo "$staged" | grep -E '\.gd$' | grep -vE '^addons/' || true) + +if [[ -n "$gd_files" ]]; then + # The Godot style guide (and every file in this repo) indents with tabs. + # A stray space-indented line is almost always a paste from somewhere else + # and silently breaks nothing today but fights every future diff. + space_indented="" + while IFS= read -r f; do + [[ -f "$f" ]] || continue + if grep -nP '^\t* ' "$f" | grep -qv '^\s*#'; then + space_indented+="$f"$'\n' + fi + done <<< "$gd_files" + if [[ -n "$space_indented" ]]; then + echo "FAIL: space-indented line(s) in (Godot style is tabs):" + echo "$space_indented" | sed 's/^/ /' + fail=1 + fi + + echo " running tools/check.sh (parse check)..." + if ! tools/check.sh; then + fail=1 + fi + + echo " running tools/test.sh (GUT suite)..." + if ! tools/test.sh; then + fail=1 + fi +fi + +if [[ $fail -ne 0 ]]; then + echo + echo "pre-commit FAILED. Fix the above, or skip deliberately with --no-verify." + exit 1 +fi + +echo "pre-commit OK" diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..2fd869e --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# The one check that needs a real socket: boot a dedicated server, connect two +# bot clients over ENet, and assert the whole loop happened. ~35s, so it runs +# on push rather than every commit. +# +# Skip deliberately (e.g. pushing docs-only or WIP) with: +# SKIP_SMOKE_HOOK=1 git push +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" + +if [[ "${SKIP_SMOKE_HOOK:-0}" == "1" ]]; then + echo "pre-push: SKIP_SMOKE_HOOK=1, skipping smoke test" + exit 0 +fi + +echo "pre-push: running tools/smoke.sh..." +if ! tools/smoke.sh; then + echo + echo "pre-push FAILED: end-to-end smoke test did not pass." + echo "Push anyway with: SKIP_SMOKE_HOOK=1 git push" + exit 1 +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..92c929c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: ci + +on: + push: + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + # Image ships the Godot 4 headless binary as `godot`. + container: + image: barichello/godot-ci:4.7.2 + steps: + - uses: actions/checkout@v4 + + # Populates .godot/, including the global script class cache. Every later + # step depends on this; without it every class_name looks undeclared. + - name: Import project + run: godot --headless --path . --import + + - name: Parse check + run: SKIP_IMPORT=1 tools/check.sh + + - name: Unit and integration tests + run: SKIP_IMPORT=1 tools/test.sh + + # The only step that exercises RPC wiring, the wire codec and instance + # transfers. Uses a real ENet socket on loopback. + - name: End-to-end smoke test + run: tools/smoke.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c318b1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Godot's import cache and editor state. Regenerated by `godot --import`. +.godot/ +# Export artefacts. +build/ +*.pck +*.zip +# Editor-local overrides. +override.cfg +# Claude Code local settings (permissions differ per machine). +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..895ea7a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# Transcience + +Top-down twin-stick bullet-hell with a dedicated, server-authoritative backend. +Godot 4.7, GDScript only. One executable is both server and client. + +## Commands + +```bash +tools/check.sh # parse-check every script (~5s) -- run after every edit +tools/test.sh # GUT suite, headless (~2s) +tools/smoke.sh # real server + 2 bot clients over ENet (~35s) +tools/bench.gd # godot --headless --path . --script tools/bench.gd +tools/server.sh # dedicated server +tools/client.sh --join --name ada +``` + +Everything after `--` goes to `GameOpts.parse()`: + +| Flag | Effect | +| --- | --- | +| `--server` | Dedicated server, no view. | +| `--join` | Skip the menu and connect. | +| `--listen` | Skip the menu and host a listen server with a local player. | +| `--bot` | Scripted input instead of the keyboard. Implies `--join`. | +| `--host`, `--port`, `--name` | Connection details. | +| `--autoquit N` | Quit after N physics ticks. | +| `--boss-rush` | Server-side: new dungeons open straight onto the boss. | +| `--verbose` / `--quiet` | Log level. | + +A change is done when `check.sh`, `test.sh` and — if it touched networking, +instances or the simulation — `smoke.sh` all pass. Say so explicitly; do not +report a networking change as working on the strength of unit tests alone. + +## Hooks + +`tools/install-hooks.sh` once per clone (points `core.hooksPath` at +`.githooks/`, committed in the repo — plain bash, no `pre-commit` framework, so +cloning costs nothing extra to run tools/*.sh). Own `.gd` files (not vendored +`addons/`) trigger `check.sh` + `test.sh` on commit (~7s); `smoke.sh` runs on +push (~35s, skip deliberately with `SKIP_SMOKE_HOOK=1 git push`). + +## The one rule + +**The server decides everything; the client only sends intent.** + +A client can send exactly two things: an [InputFrame] (move vector, aim angle, +three button bits) and a handshake. There is no message for "I moved here", "I +hit that", "I took damage" or "my escape finished". Adding one would collapse +the whole security model, so don't — validate-after-the-fact is strictly weaker +than having no code path at all. + +`SimWorld.authoritative` is `true` on the server and `false` on the client. In +replica mode the world runs no AI, fires no emitters and resolves no hits; it +only integrates bullets it was told about. `tests/unit/test_server_authority.gd` +and `tests/integration/test_replica_parity.gd` pin this down. + +## Layout + +| Path | What lives there | +| --- | --- | +| `src/sim/` | The whole game as plain RefCounted objects. No nodes, no physics server, no rendering. | +| `src/sim/patterns/` | Bullet emitters — the authoring surface for every enemy and boss. | +| `src/content/content.gd` | All enemies and bosses, defined in code. Source of truth. | +| `src/net/` | Codec, `ServerRuntime`, `ClientRuntime`. | +| `src/instances/` | Lobby hub and dungeon runs. | +| `src/view/`, `src/ui/` | Read-only rendering. Never decides anything. | +| `src/autoload/net.gd` | The only autoload. RPC surface. | +| `tools/` | Headless tooling. | + +The simulation must not import anything from `src/net/`, `src/view/` or +`src/ui/`, and must not touch `Net`. That is what lets tests drive a thousand +ticks in milliseconds with no SceneTree. + +## Godot gotchas that will waste your time + +1. **New `class_name` needs a cache refresh.** `.godot/global_script_class_cache.cfg` + is only rebuilt by the editor or `godot --headless --path . --import`. + Until then every use of the new class reports `Identifier not declared`, + which looks like a real error. `check.sh` does the refresh for you. +2. **Autoload names do not resolve under `--script`.** A `--script` run has no + main loop, so `Net` is `Identifier not found`. Tools that need autoloads must + run as a scene (see `tools/check.tscn`); tools that don't can use `--script`. + This is why `GameLog` and `GameOpts` are static classes rather than autoloads. +3. **`ResourceLoader.load()` returns non-null for a broken script.** Never test + the return value to detect a parse error; read the engine's stderr instead. + And never call `Script.reload()` on the script you are running — it hangs. +4. **Input events default to `device = 16`, which matches nothing.** Bindings + must use `device = -1`. `tools/setup_input_map.gd` generates the input map + correctly; edit that file, not the `[input]` block in `project.godot`. +5. **`ProjectSettings.save()` drops settings equal to the engine default** and + strips comments. Anything load-bearing (the 60 Hz tick) is asserted in code + in `src/main.gd` instead of trusted to `project.godot`. + +## Adding content + +A new enemy or boss is data, never code. Add a builder to +`src/content/content.gd` returning an `EnemyDef` / `BossDef` made of the +emitters in `src/sim/patterns/`, register its id in `enemy()` / `boss()`, and +add a test. `tests/unit/test_boss.gd::test_a_brand_new_boss_needs_no_engine_changes` +builds a boss from scratch and asserts the simulation needs no changes to run +it — if you find yourself adding a per-boss branch to `SimWorld`, stop and add +an emitter type instead. + +`tools/export_content.gd` writes `.tres` copies into `resources/` for tuning in +the editor inspector. Those are an export, not the source; port changes back. + +## Style + +Typed GDScript everywhere (`untyped_declaration` is a warning). Tabs, snake_case +files, `PascalCase` class names. Comments explain *why* a thing is the way it is — +the netcode and anti-cheat decisions especially. Keep the existing density. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef0b093 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Transcience + +Top-down twin-stick bullet-hell with a dedicated, server-authoritative backend. +Godot 4.7, GDScript, no external runtime dependencies. + +## Play + +```bash +tools/server.sh # dedicated server on :27015 +tools/client.sh --join --name ada # connect a client +``` + +Or run one client and press **Host and play** for a listen server. + +**Controls** — WASD move, mouse aim, LMB fire, **E** on the ring in the hub to +enter a dungeon, **hold F** for three seconds to escape back to the hub. + +## Develop + +```bash +tools/check.sh # parse-check every script ~5s +tools/test.sh # GUT suite, headless ~2s +tools/smoke.sh # server + 2 bot clients, ENet ~35s +godot --headless --path . --script tools/bench.gd +``` + +Read [CLAUDE.md](CLAUDE.md) first — it is short, and the Godot CLI gotchas in it +will otherwise cost an afternoon. + +## What is here + +| | | +| --- | --- | +| **Twin-stick bullet hell** | Server-simulated bullet field, ~350 concurrent bullets at 0.24 ms/tick. | +| **Authoritative multiplayer** | Clients send input only. Hits, damage, death and instance transfers are server decisions. Prediction + reconciliation for the local player. | +| **Enemies** | Four readable behaviours (static, drift, orbit, approach, strafe) composed with bullet emitters. | +| **Boss** | The Warden of the Fold: stationary, four phases, each adding one idea. Defined entirely as data. | +| **Lobby hub** | Shared persistent instance with a portal into dungeon runs. | +| **Emergency escape** | Three-second channel back to the hub, cancelled by damage. | + +## Docs + +- [CLAUDE.md](CLAUDE.md) — commands, invariants, Godot CLI gotchas +- [docs/WORKFLOW.md](docs/WORKFLOW.md) — agent-assisted Godot development, and why the setup looks like this +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — code map and the node-free simulation +- [docs/NETCODE.md](docs/NETCODE.md) — replication model, hit validation, known gaps +- [docs/ROADMAP.md](docs/ROADMAP.md) — what is built and what is next diff --git a/addons/gut/GutScene.gd b/addons/gut/GutScene.gd new file mode 100644 index 0000000..abb9689 --- /dev/null +++ b/addons/gut/GutScene.gd @@ -0,0 +1,132 @@ +extends Node2D +# ############################################################################## +# This is a wrapper around the normal and compact gui controls and serves as +# the interface between gut.gd and the gui. The GutRunner creates an instance +# of this and then this takes care of managing the different GUI controls. +# ############################################################################## +@onready var _normal_gui = $Normal +@onready var _compact_gui = $Compact + +var gut = null : + set(val): + gut = val + _set_gut(val) + + +func _ready(): + _normal_gui.switch_modes.connect(use_compact_mode.bind(true)) + _compact_gui.switch_modes.connect(use_compact_mode.bind(false)) + + _normal_gui.set_title("GUT") + _compact_gui.set_title("GUT") + + _normal_gui.align_right() + _compact_gui.to_bottom_right() + + use_compact_mode(false) + + if(get_parent() == get_tree().root): + _test_running_setup() + +func _test_running_setup(): + set_font_size(100) + _normal_gui.get_textbox().text = "hello world, how are you doing?" + +# ------------------------ +# Private +# ------------------------ +func _set_gut(val): + if(_normal_gui.get_gut() == val): + return + _normal_gui.set_gut(val) + _compact_gui.set_gut(val) + + val.start_run.connect(_on_gut_start_run) + val.end_run.connect(_on_gut_end_run) + val.start_pause_before_teardown.connect(_on_gut_pause) + val.end_pause_before_teardown.connect(_on_pause_end) + +func _set_both_titles(text): + _normal_gui.set_title(text) + _compact_gui.set_title(text) + + +# ------------------------ +# Events +# ------------------------ +func _on_gut_start_run(): + _set_both_titles('Running') + +func _on_gut_end_run(): + _set_both_titles('Finished') + +func _on_gut_pause(): + _set_both_titles('-- Paused --') + +func _on_pause_end(): + _set_both_titles('Running') + + +# ------------------------ +# Public +# ------------------------ +func get_textbox(): + return _normal_gui.get_textbox() + + +func set_font_size(new_size): + var rtl = _normal_gui.get_textbox() + + rtl.set('theme_override_font_sizes/bold_italics_font_size', new_size) + rtl.set('theme_override_font_sizes/bold_font_size', new_size) + rtl.set('theme_override_font_sizes/italics_font_size', new_size) + rtl.set('theme_override_font_sizes/normal_font_size', new_size) + + +func set_font(font_name): + _set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name) + + +func _set_font(rtl, font_name, custom_name): + if(font_name == null): + rtl.remove_theme_font_override(custom_name) + else: + var font_path = 'res://addons/gut/fonts/' + font_name + '.ttf' + if(FileAccess.file_exists(font_path)): + var dyn_font = FontFile.new() + dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf') + rtl.add_theme_font_override(custom_name, dyn_font) + + +func _set_all_fonts_in_rtl(rtl, base_name): + if(base_name == 'Default'): + _set_font(rtl, null, 'normal_font') + _set_font(rtl, null, 'bold_font') + _set_font(rtl, null, 'italics_font') + _set_font(rtl, null, 'bold_italics_font') + else: + _set_font(rtl, base_name + '-Regular', 'normal_font') + _set_font(rtl, base_name + '-Bold', 'bold_font') + _set_font(rtl, base_name + '-Italic', 'italics_font') + _set_font(rtl, base_name + '-BoldItalic', 'bold_italics_font') + + +func set_default_font_color(color): + _normal_gui.get_textbox().set('custom_colors/default_color', color) + + +func set_background_color(color): + _normal_gui.set_bg_color(color) + + +func use_compact_mode(should=true): + _compact_gui.visible = should + _normal_gui.visible = !should + + +func set_opacity(val): + _normal_gui.modulate.a = val + _compact_gui.modulate.a = val + +func set_title(text): + _set_both_titles(text) diff --git a/addons/gut/GutScene.gd.uid b/addons/gut/GutScene.gd.uid new file mode 100644 index 0000000..46e710f --- /dev/null +++ b/addons/gut/GutScene.gd.uid @@ -0,0 +1 @@ +uid://bw7tukh738kw1 diff --git a/addons/gut/GutScene.tscn b/addons/gut/GutScene.tscn new file mode 100644 index 0000000..82638cc --- /dev/null +++ b/addons/gut/GutScene.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=4 format=3 uid="uid://m28heqtswbuq"] + +[ext_resource type="Script" uid="uid://bw7tukh738kw1" path="res://addons/gut/GutScene.gd" id="1_b4m8y"] +[ext_resource type="PackedScene" uid="uid://duxblir3vu8x7" path="res://addons/gut/gui/NormalGui.tscn" id="2_j6ywb"] +[ext_resource type="PackedScene" uid="uid://cnqqdfsn80ise" path="res://addons/gut/gui/MinGui.tscn" id="3_3glw1"] + +[node name="GutScene" type="Node2D"] +script = ExtResource("1_b4m8y") + +[node name="Normal" parent="." instance=ExtResource("2_j6ywb")] + +[node name="Compact" parent="." instance=ExtResource("3_3glw1")] +offset_left = 5.0 +offset_top = 273.0 +offset_right = 265.0 +offset_bottom = 403.0 diff --git a/addons/gut/LICENSE.md b/addons/gut/LICENSE.md new file mode 100644 index 0000000..a38ac23 --- /dev/null +++ b/addons/gut/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) +===================== + +Copyright (c) 2018 Tom "Butch" Wesley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/gut/UserFileViewer.gd b/addons/gut/UserFileViewer.gd new file mode 100644 index 0000000..73b6a00 --- /dev/null +++ b/addons/gut/UserFileViewer.gd @@ -0,0 +1,52 @@ +extends Window + +@onready var rtl = $TextDisplay/RichTextLabel + +func _get_file_as_text(path): + var to_return = null + var f = FileAccess.open(path, FileAccess.READ) + if(f != null): + to_return = f.get_as_text() + else: + to_return = str('ERROR: Could not open file. Error code ', FileAccess.get_open_error()) + return to_return + +func _ready(): + rtl.clear() + +func _on_OpenFile_pressed(): + $FileDialog.popup_centered() + +func _on_FileDialog_file_selected(path): + show_file(path) + +func _on_Close_pressed(): + self.hide() + +func show_file(path): + var text = _get_file_as_text(path) + if(text == ''): + text = '' + rtl.set_text(text) + self.window_title = path + +func show_open(): + self.popup_centered() + $FileDialog.popup_centered() + +func get_rich_text_label(): + return $TextDisplay/RichTextLabel + +func _on_Home_pressed(): + rtl.scroll_to_line(0) + +func _on_End_pressed(): + rtl.scroll_to_line(rtl.get_line_count() -1) + +func _on_Copy_pressed(): + return + # OS.clipboard = rtl.text + +func _on_file_dialog_visibility_changed(): + if rtl.text.length() == 0 and not $FileDialog.visible: + self.hide() diff --git a/addons/gut/UserFileViewer.gd.uid b/addons/gut/UserFileViewer.gd.uid new file mode 100644 index 0000000..50a409a --- /dev/null +++ b/addons/gut/UserFileViewer.gd.uid @@ -0,0 +1 @@ +uid://x51wilphva3d diff --git a/addons/gut/UserFileViewer.tscn b/addons/gut/UserFileViewer.tscn new file mode 100644 index 0000000..15e2e86 --- /dev/null +++ b/addons/gut/UserFileViewer.tscn @@ -0,0 +1,92 @@ +[gd_scene load_steps=2 format=3 uid="uid://bsm7wtt1gie4v"] + +[ext_resource type="Script" uid="uid://x51wilphva3d" path="res://addons/gut/UserFileViewer.gd" id="1"] + +[node name="UserFileViewer" type="Window"] +exclusive = true +script = ExtResource("1") + +[node name="FileDialog" type="FileDialog" parent="."] +access = 1 +show_hidden_files = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TextDisplay" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_right = -10.0 +offset_bottom = -65.0 +color = Color(0.2, 0.188235, 0.188235, 1) + +[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"] +anchor_right = 1.0 +anchor_bottom = 1.0 +focus_mode = 2 +text = "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. + +Lorem 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. + +Versions 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." +selection_enabled = true + +[node name="OpenFile" type="Button" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -158.0 +offset_top = -50.0 +offset_right = -84.0 +offset_bottom = -30.0 +text = "Open File" + +[node name="Home" type="Button" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -478.0 +offset_top = -50.0 +offset_right = -404.0 +offset_bottom = -30.0 +text = "Home" + +[node name="Copy" type="Button" parent="."] +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 160.0 +offset_top = -50.0 +offset_right = 234.0 +offset_bottom = -30.0 +text = "Copy" + +[node name="End" type="Button" parent="."] +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -318.0 +offset_top = -50.0 +offset_right = -244.0 +offset_bottom = -30.0 +text = "End" + +[node name="Close" type="Button" parent="."] +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 10.0 +offset_top = -50.0 +offset_right = 80.0 +offset_bottom = -30.0 +text = "Close" + +[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"] +[connection signal="visibility_changed" from="FileDialog" to="." method="_on_file_dialog_visibility_changed"] +[connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"] +[connection signal="pressed" from="Home" to="." method="_on_Home_pressed"] +[connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"] +[connection signal="pressed" from="End" to="." method="_on_End_pressed"] +[connection signal="pressed" from="Close" to="." method="_on_Close_pressed"] diff --git a/addons/gut/autofree.gd b/addons/gut/autofree.gd new file mode 100644 index 0000000..240cd93 --- /dev/null +++ b/addons/gut/autofree.gd @@ -0,0 +1,86 @@ +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# Class used to keep track of objects to be freed and utilities to free them. +# ############################################################################## +var _to_free = [] +var _to_queue_free = [] +var _ref_counted_doubles = [] +var _all_instance_ids = [] + + +func _add_instance_id(thing): + if(thing.has_method("get_instance_id")): + _all_instance_ids.append(thing.get_instance_id()) + + +func add_free(thing): + if(typeof(thing) == TYPE_OBJECT): + _add_instance_id(thing) + if(!thing is RefCounted): + _to_free.append(thing) + elif(GutUtils.is_double(thing)): + _ref_counted_doubles.append(thing) + + +func add_queue_free(thing): + if(typeof(thing) == TYPE_OBJECT): + _add_instance_id(thing) + _to_queue_free.append(thing) + + +func get_queue_free_count(): + return _to_queue_free.size() + + +func get_free_count(): + return _to_free.size() + + +func free_all(): + for node in _to_free: + if(is_instance_valid(node)): + if(GutUtils.is_double(node)): + node.__gutdbl_done() + node.free() + _to_free.clear() + + for i in range(_to_queue_free.size()): + if(is_instance_valid(_to_queue_free[i])): + _to_queue_free[i].queue_free() + _to_queue_free.clear() + + for ref_dbl in _ref_counted_doubles: + ref_dbl.__gutdbl_done() + _ref_counted_doubles.clear() + + _all_instance_ids.clear() + + +func has_instance_id(id): + return _all_instance_ids.has(id) \ No newline at end of file diff --git a/addons/gut/autofree.gd.uid b/addons/gut/autofree.gd.uid new file mode 100644 index 0000000..13162c8 --- /dev/null +++ b/addons/gut/autofree.gd.uid @@ -0,0 +1 @@ +uid://bxjfriqxgwe0r diff --git a/addons/gut/awaiter.gd b/addons/gut/awaiter.gd new file mode 100644 index 0000000..6c23442 --- /dev/null +++ b/addons/gut/awaiter.gd @@ -0,0 +1,201 @@ +extends Node + +class GutAwaiterLogger: + var _time_waited = 0.0 + var logger = GutUtils.get_logger() + var waiting_on = "nothing" + var logged_initial_message = false + var wait_log_delay := 1.0 + var disabled = false + + func waited(x): + _time_waited += x + if(!logged_initial_message and _time_waited >= wait_log_delay): + log_it() + logged_initial_message = true + + + func reset(): + _time_waited = 0.0 + logged_initial_message = false + + + func log_it(): + if(!disabled): + var msg = str("--- Awaiting ", waiting_on, " ---") + logger.wait_msg(msg) + + + + +signal timeout +signal wait_started + +var await_logger = GutAwaiterLogger.new() +var _wait_time := 0.0 +var _wait_process_frames := 0 +var _wait_physics_frames := 0 +var _signal_to_wait_on = null + +var _predicate_method = null +var _waiting_for_predicate_to_be = null + +var _predicate_time_between := 0.0 +var _predicate_time_between_elpased := 0.0 + +var _elapsed_time := 0.0 +var _elapsed_frames := 0 + +var _did_last_wait_timeout = false +var did_last_wait_timeout = false : + get: return _did_last_wait_timeout + set(val): push_error("Cannot set did_last_wait_timeout") + + + +func _ready() -> void: + get_tree().process_frame.connect(_on_tree_process_frame) + get_tree().physics_frame.connect(_on_tree_physics_frame) + + +func _on_tree_process_frame(): + # Count frames here instead of in _process so that tree order never + # makes a difference and the count/signaling happens outside of + # _process being called. + if(_wait_process_frames > 0): + _elapsed_frames += 1 + if(_elapsed_frames > _wait_process_frames): + _end_wait() + + +func _on_tree_physics_frame(): + # Count frames here instead of in _physics_process so that tree order never + # makes a difference and the count/signaling happens outside of + # _physics_process being called. + if(_wait_physics_frames != 0): + _elapsed_frames += 1 + if(_elapsed_frames > _wait_physics_frames): + _end_wait() + + +func _physics_process(delta): + if(is_waiting()): + await_logger.waited(delta) + + if(_wait_time != 0.0): + _elapsed_time += delta + if(_elapsed_time >= _wait_time): + _end_wait() + + if(_predicate_method != null): + _predicate_time_between_elpased += delta + if(_predicate_time_between_elpased >= _predicate_time_between): + _predicate_time_between_elpased = 0.0 + var result = _predicate_method.call() + if(_waiting_for_predicate_to_be == false): + if(typeof(result) != TYPE_BOOL or result != true): + _end_wait() + else: + if(typeof(result) == TYPE_BOOL and result == _waiting_for_predicate_to_be): + _end_wait() + + +func _end_wait(): + await_logger.reset() + # Check for time before checking for frames so that the extra frames added + # when waiting on a signal do not cause a false negative for timing out. + if(_wait_time > 0): + _did_last_wait_timeout = _elapsed_time >= _wait_time + elif(_wait_physics_frames > 0): + _did_last_wait_timeout = _elapsed_frames >= _wait_physics_frames + elif(_wait_process_frames > 0): + _did_last_wait_timeout = _elapsed_frames >= _wait_process_frames + + if(_signal_to_wait_on != null and \ + is_instance_valid(_signal_to_wait_on.get_object()) and \ + _signal_to_wait_on.is_connected(_signal_callback)): + _signal_to_wait_on.disconnect(_signal_callback) + + _wait_process_frames = 0 + _wait_time = 0.0 + _wait_physics_frames = 0 + _signal_to_wait_on = null + _predicate_method = null + _elapsed_time = 0.0 + _elapsed_frames = 0 + timeout.emit() + + +const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' +func _signal_callback( + _arg1=ARG_NOT_SET, _arg2=ARG_NOT_SET, _arg3=ARG_NOT_SET, + _arg4=ARG_NOT_SET, _arg5=ARG_NOT_SET, _arg6=ARG_NOT_SET, + _arg7=ARG_NOT_SET, _arg8=ARG_NOT_SET, _arg9=ARG_NOT_SET): + + _signal_to_wait_on.disconnect(_signal_callback) + # DO NOT _end_wait here. For other parts of the test to get the signal that + # was waited on, we have to wait for another frames. For example, the + # signal_watcher doesn't get the signal in time if we don't do this. + _wait_process_frames = 1 + + +func wait_seconds(x, msg=''): + await_logger.waiting_on = str(x, " seconds ", msg) + _did_last_wait_timeout = false + _wait_time = x + wait_started.emit() + + +func wait_process_frames(x, msg=''): + await_logger.waiting_on = str(x, " idle frames ", msg) + _did_last_wait_timeout = false + _wait_process_frames = x + wait_started.emit() + + +func wait_physics_frames(x, msg=''): + await_logger.waiting_on = str(x, " physics frames ", msg) + _did_last_wait_timeout = false + _wait_physics_frames = x + wait_started.emit() + + +func wait_for_signal(the_signal : Signal, max_time, msg=''): + await_logger.waiting_on = str("signal ", the_signal.get_name(), " or ", max_time, "s ", msg) + _did_last_wait_timeout = false + the_signal.connect(_signal_callback) + _signal_to_wait_on = the_signal + _wait_time = max_time + wait_started.emit() + + +func wait_until(predicate_function: Callable, max_time, time_between_calls:=0.0, msg=''): + await_logger.waiting_on = str("callable to return TRUE or ", max_time, "s. ", msg) + _predicate_time_between = time_between_calls + _predicate_method = predicate_function + _wait_time = max_time + + _waiting_for_predicate_to_be = true + _predicate_time_between_elpased = 0.0 + _did_last_wait_timeout = false + + wait_started.emit() + + +func wait_while(predicate_function: Callable, max_time, time_between_calls:=0.0, msg=''): + await_logger.waiting_on = str("callable to return FALSE or ", max_time, "s. ", msg) + _predicate_time_between = time_between_calls + _predicate_method = predicate_function + _wait_time = max_time + + _waiting_for_predicate_to_be = false + _predicate_time_between_elpased = 0.0 + _did_last_wait_timeout = false + + wait_started.emit() + + +func is_waiting(): + return _wait_time != 0.0 or \ + _wait_physics_frames != 0 or \ + _wait_process_frames != 0 diff --git a/addons/gut/awaiter.gd.uid b/addons/gut/awaiter.gd.uid new file mode 100644 index 0000000..d8cae05 --- /dev/null +++ b/addons/gut/awaiter.gd.uid @@ -0,0 +1 @@ +uid://ccu4ww35edtdi diff --git a/addons/gut/cli/change_project_warnings.gd b/addons/gut/cli/change_project_warnings.gd new file mode 100644 index 0000000..9dcf7d2 --- /dev/null +++ b/addons/gut/cli/change_project_warnings.gd @@ -0,0 +1,239 @@ +extends SceneTree + +var Optparse = load('res://addons/gut/cli/optparse.gd') +var WarningsManager = load("res://addons/gut/warnings_manager.gd") +const WARN_VALUE_PRINT_POSITION = 36 + +var godot_default_warnings = { + "assert_always_false": 1, "assert_always_true": 1, "confusable_identifier": 1, + "confusable_local_declaration": 1, "confusable_local_usage": 1, "constant_used_as_function": 1, + "deprecated_keyword": 1, "empty_file": 1, "enable": true, + "exclude_addons": true, "function_used_as_property": 1, "get_node_default_without_onready": 2, + "incompatible_ternary": 1, "inference_on_variant": 2, "inferred_declaration": 0, + "int_as_enum_without_cast": 1, "int_as_enum_without_match": 1, "integer_division": 1, + "narrowing_conversion": 1, "native_method_override": 2, "onready_with_export": 2, + "property_used_as_function": 1, "redundant_await": 1, "redundant_static_unload": 1, + "renamed_in_godot_4_hint": 1, "return_value_discarded": 0, "shadowed_global_identifier": 1, + "shadowed_variable": 1, "shadowed_variable_base_class": 1, "standalone_expression": 1, + "standalone_ternary": 1, "static_called_on_instance": 1, "unassigned_variable": 1, + "unassigned_variable_op_assign": 1, "unreachable_code": 1, "unreachable_pattern": 1, + "unsafe_call_argument": 0, "unsafe_cast": 0, "unsafe_method_access": 0, + "unsafe_property_access": 0, "unsafe_void_return": 1, "untyped_declaration": 0, + "unused_local_constant": 1, "unused_parameter": 1, "unused_private_class_variable": 1, + "unused_signal": 1, "unused_variable": 1 +} + +var gut_default_changes = { + "exclude_addons": false, "redundant_await": 0, +} + +var warning_settings = {} + +func _setup_warning_settings(): + warning_settings["godot_default"] = godot_default_warnings + warning_settings["current"] = WarningsManager.create_warnings_dictionary_from_project_settings() + warning_settings["all_warn"] = WarningsManager.create_warn_all_warnings_dictionary() + + var gut_default = godot_default_warnings.duplicate() + gut_default.merge(gut_default_changes, true) + warning_settings["gut_default"] = gut_default + + +func _warn_value_to_s(value): + var readable = str(value).capitalize() + if(typeof(value) == TYPE_INT): + readable = WarningsManager.WARNING_LOOKUP.get(value, str(readable, ' ???')) + readable = readable.capitalize() + return readable + + +func _human_readable(warnings): + var to_return = "" + for key in warnings: + var readable = _warn_value_to_s(warnings[key]) + to_return += str(key.capitalize().rpad(35, ' '), readable, "\n") + return to_return + + +func _dump_settings(which): + if(warning_settings.has(which)): + GutUtils.pretty_print(warning_settings[which]) + else: + print("UNKNOWN print option ", which) + + +func _print_settings(which): + if(warning_settings.has(which)): + print(_human_readable(warning_settings[which])) + else: + print("UNKNOWN print option ", which) + + +func _apply_settings(which): + if(!warning_settings.has(which)): + print("UNKNOWN set option ", which) + return + + var pre_settings = warning_settings["current"] + var new_settings = warning_settings[which] + + if(new_settings == pre_settings): + print("-- Settings are the same, no changes were made --") + return + + WarningsManager.apply_warnings_dictionary(new_settings) + ProjectSettings.save() + print("-- Project Warning Settings have been updated --") + print(_diff_changes_text(pre_settings)) + + +func _diff_text(w1, w2, diff_col_pad=10): + var to_return = "" + for key in w1: + var v1_text = _warn_value_to_s(w1[key]) + var v2_text = _warn_value_to_s(w2[key]) + var diff_text = v1_text + var prefix = " " + + if(v1_text != v2_text): + var diff_prefix = " " + if(w1[key] > w2[key]): + diff_prefix = "-" + else: + diff_prefix = "+" + prefix = "* " + diff_text = str(v1_text.rpad(diff_col_pad, ' '), diff_prefix, v2_text) + + to_return += str(str(prefix, key.capitalize()).rpad(WARN_VALUE_PRINT_POSITION, ' '), diff_text, "\n") + + return to_return.rstrip("\n") + + +func _diff_changes_text(pre_settings): + var orig_diff_text = _diff_text( + pre_settings, + WarningsManager.create_warnings_dictionary_from_project_settings(), + 0) + # these next two lines are fragile and brute force...enjoy + var diff_text = orig_diff_text.replace("-", " -> ") + diff_text = diff_text.replace("+", " -> ") + + if(orig_diff_text == diff_text): + diff_text += "\n-- No changes were made --" + else: + diff_text += "\nChanges will not be visible in Godot until it is restarted.\n" + diff_text += "Even if it asks you to reload...Maybe. Probably." + + return diff_text + + + +func _diff(name_1, name_2): + if(warning_settings.has(name_1) and warning_settings.has(name_2)): + var c2_pad = name_1.length() + 2 + var heading = str(" ".repeat(WARN_VALUE_PRINT_POSITION), name_1.rpad(c2_pad, ' '), name_2, "\n") + heading += str( + " ".repeat(WARN_VALUE_PRINT_POSITION), + "-".repeat(name_1.length()).rpad(c2_pad, " "), + "-".repeat(name_2.length()), + "\n") + + var text = _diff_text(warning_settings[name_1], warning_settings[name_2], c2_pad) + + print(heading) + print(text) + + var diff_count = 0 + for line in text.split("\n"): + if(!line.begins_with(" ")): + diff_count += 1 + + if(diff_count == 0): + print('-- [', name_1, "] and [", name_2, "] are the same --") + else: + print('-- There are ', diff_count, ' differences between [', name_1, "] and [", name_2, "] --") + else: + print("One or more unknown Warning Level Names:, [", name_1, "] [", name_2, "]") + + +func _set_settings(nvps): + var pre_settings = warning_settings["current"] + for i in range(nvps.size()/2): + var s_name = nvps[i * 2] + var s_value = nvps[i * 2 + 1] + if(godot_default_warnings.has(s_name)): + var t = typeof(godot_default_warnings[s_name]) + if(t == TYPE_INT): + s_value = s_value.to_int() + elif(t == TYPE_BOOL): + s_value = s_value.to_lower() == 'true' + + WarningsManager.set_project_setting_warning(s_name, s_value) + ProjectSettings.save() + print(_diff_changes_text(pre_settings)) + + + +func _setup_options(): + var opts = Optparse.new() + opts.banner = """ + This script prints info about or sets the warning settings for the project. + Each action requires one or more Warning Level Names. + + Warning Level Names: + * current The current settings for the project. + * godot_default The default settings for Godot. + * gut_default The warning settings that is used when developing GUT. + * all_warn Everything set to warn. + """.dedent() + + opts.add('-h', false, 'Print this help') + opts.add('-set', [], "Sets a single setting in the project settings and saves.\n" + + "Use -dump to see a list of setting names and values.\n" + + "Example: -set enabled,true -set unsafe_cast,2 -set unreachable_code,0") + opts.add_heading(" Actions (require Warning Level Name)") + opts.add('-diff', [], "Shows the difference between two Warning Level Names.\n" + + "Example: -diff current,all_warn") + opts.add('-dump', 'none', "Prints a dictionary of the warning values.") + opts.add('-print', 'none', "Print human readable warning values.") + opts.add('-apply', 'none', "Applys one of the Warning Level Names to the project settings. You should restart after using this") + + return opts + +func _print_help(opts): + opts.print_help() + + + +func _init(): + # Testing might set this flag but it should never be disabled for this tool + # or it cannot save project settings, but says it did. Sneakily use the + # private property to get around this property being read-only. Don't + # try this at home. + WarningsManager._disabled = false + + _setup_warning_settings() + + var opts = _setup_options() + opts.parse() + + if(opts.unused.size() != 0): + opts.print_help() + print("Unknown arguments ", opts.unused) + if(opts.values.h): + opts.print_help() + elif(opts.values.print != 'none'): + _print_settings(opts.values.print) + elif(opts.values.dump != 'none'): + _dump_settings(opts.values.dump) + elif(opts.values.apply != 'none'): + _apply_settings(opts.values.apply ) + elif(opts.values.diff.size() == 2): + _diff(opts.values.diff[0], opts.values.diff[1]) + elif(opts.values.set.size() % 2 == 0): + _set_settings(opts.values.set) + else: + opts.print_help() + print("You didn't specify any options or too many or not the right size or something invalid. I don't know what you want to do.") + + quit() \ No newline at end of file diff --git a/addons/gut/cli/change_project_warnings.gd.uid b/addons/gut/cli/change_project_warnings.gd.uid new file mode 100644 index 0000000..c8c765d --- /dev/null +++ b/addons/gut/cli/change_project_warnings.gd.uid @@ -0,0 +1 @@ +uid://1pauyfnd1cre diff --git a/addons/gut/cli/gut_cli.gd b/addons/gut/cli/gut_cli.gd new file mode 100644 index 0000000..4bc17de --- /dev/null +++ b/addons/gut/cli/gut_cli.gd @@ -0,0 +1,328 @@ +extends Node + +var Optparse = load('res://addons/gut/cli/optparse.gd') +var Gut = load('res://addons/gut/gut.gd') +var GutRunner = load('res://addons/gut/gui/GutRunner.tscn') + +# ------------------------------------------------------------------------------ +# Helper class to resolve the various different places where an option can +# be set. Using the get_value method will enforce the order of precedence of: +# 1. command line value +# 2. config file value +# 3. default value +# +# The idea is that you set the base_opts. That will get you a copies of the +# hash with null values for the other types of values. Lower precedented hashes +# will punch through null values of higher precedented hashes. +# ------------------------------------------------------------------------------ +class GutCliOptionResolver: + var base_opts = {} + var cmd_opts = {} + var config_opts = {} + + + func get_value(key): + return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key])) + + func set_base_opts(opts): + base_opts = opts + cmd_opts = _null_copy(opts) + config_opts = _null_copy(opts) + + # creates a copy of a hash with all values null. + func _null_copy(h): + var new_hash = {} + for key in h: + new_hash[key] = null + return new_hash + + func _nvl(a, b): + if(a == null): + return b + else: + return a + + func _string_it(h): + var to_return = '' + for key in h: + to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')') + return to_return + + func to_s(): + return str("base:\n", _string_it(base_opts), "\n", \ + "config:\n", _string_it(config_opts), "\n", \ + "cmd:\n", _string_it(cmd_opts), "\n", \ + "resolved:\n", _string_it(get_resolved_values())) + + func get_resolved_values(): + var to_return = {} + for key in base_opts: + to_return[key] = get_value(key) + return to_return + + func to_s_verbose(): + var to_return = '' + var resolved = get_resolved_values() + for key in base_opts: + to_return += str(key, "\n") + to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n") + to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n") + to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n") + to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n") + + return to_return + +# ------------------------------------------------------------------------------ +# Here starts the actual script that uses the Options class to kick off Gut +# and run your tests. +# ------------------------------------------------------------------------------ +var _gut_config = load('res://addons/gut/gut_config.gd').new() + +# array of command line options specified +var _final_opts = [] + + +func setup_options(options, font_names): + var opts = Optparse.new() + opts.banner =\ +""" +The GUT CLI +----------- +The default behavior for GUT is to load options from a res://.gutconfig.json if +it exists. Any options specified on the command line will take precedence over +options specified in the gutconfig file. You can specify a different gutconfig +file with the -gconfig option. + +To generate a .gutconfig.json file you can use -gprint_gutconfig_sample +To see the effective values of a CLI command and a gutconfig use -gpo + +Values for options can be supplied using: + option=value # no space around "=" + option value # a space between option and value w/o = + +Options whose values are lists/arrays can be specified multiple times: + -gdir=a,b + -gdir c,d + -gdir e + # results in -gdir equaling [a, b, c, d, e] + +To not use an empty value instead of a default value, specifiy the option with +an immediate "=": + -gconfig= +""" + opts.add_heading("Test Config:") + opts.add('-gdir', options.dirs, 'List of directories to search for test scripts in.') + opts.add('-ginclude_subdirs', false, 'Flag to include all subdirectories specified with -gdir.') + opts.add('-gtest', [], 'List of full paths to test scripts to run.') + opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".') + opts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension. Default "[default]".') + opts.add('-gconfig', 'res://.gutconfig.json', 'The config file to load options from. The default is [default]. Use "-gconfig=" to not use a config file.') + opts.add('-gpre_run_script', '', 'pre-run hook script path') + opts.add('-gpost_run_script', '', 'post-run hook script path') + opts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail. With this option set, that does not happen.') + opts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"') + + opts.add_heading("Run Options:") + opts.add('-gselect', '', 'All scripts that contain the specified string in their filename will be ran') + opts.add('-ginner_class', '', 'Only run inner classes that contain the specified string in their name.') + opts.add('-gunit_test_name', '', 'Any test that contains the specified text will be run, all others will be skipped.') + opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.') + opts.add('-gexit_on_success', false, 'Only exit if zero tests fail.') + opts.add('-gignore_pause', false, 'Ignores any calls to pause_before_teardown.') + opts.add('-gno_error_tracking', false, 'Disable error tracking.') + opts.add('-gfailure_error_types', options.failure_error_types, 'Error types that will cause tests to fail if the are encountered during the execution of a test. Default "[default]"') + + opts.add_heading("Display Settings:") + opts.add('-glog', options.log_level, 'Log level [0-3]. Default [default]') + opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default [default].') + opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.') + opts.add('-gcompact_mode', false, 'The runner will be in compact mode. This overrides -gmaximize.') + opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') + opts.add('-gdisable_colors', false, 'Disable command line colors.') + opts.add('-gfont_name', options.font_name, str('Valid values are: ', font_names, '. Default "[default]"')) + opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"') + opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"') + opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"') + opts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]') + opts.add('-gwait_log_delay', options.wait_log_delay, 'Delay before GUT will print a message to indicate a test is awaiting one of the wait_* methods. Default [default]') + + opts.add_heading("Result Export:") + opts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.') + opts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, default [default]') + + opts.add_heading("Help:") + opts.add('-gh', false, 'Print this help. You did this to see this, so you probably understand.') + opts.add('-gpo', false, 'Print option values from all sources and the value used.') + opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.') + opts.add("-gcheck_update", false, "Check for update") + + # run as in editor, for shelling out purposes through Editor. + var o = opts.add('-graie', false, 'do not use') + o.show_in_help = false + return opts + + +# Parses options, applying them to the _tester or setting values +# in the options struct. +func extract_command_line_options(from, to): + to.compact_mode = from.get_value_or_null('-gcompact_mode') + to.config_file = from.get_value_or_null('-gconfig') + to.dirs = from.get_value_or_null('-gdir') + to.disable_colors = from.get_value_or_null('-gdisable_colors') + to.double_strategy = from.get_value_or_null('-gdouble_strategy') + to.errors_do_not_cause_failure = from.get_value_or_null('-gerrors_do_not_cause_failure') + to.hide_orphans = from.get_value_or_null('-ghide_orphans') + to.ignore_pause = from.get_value_or_null('-gignore_pause') + to.include_subdirs = from.get_value_or_null('-ginclude_subdirs') + to.inner_class = from.get_value_or_null('-ginner_class') + to.log_level = from.get_value_or_null('-glog') + to.opacity = from.get_value_or_null('-gopacity') + to.post_run_script = from.get_value_or_null('-gpost_run_script') + to.pre_run_script = from.get_value_or_null('-gpre_run_script') + to.prefix = from.get_value_or_null('-gprefix') + to.selected = from.get_value_or_null('-gselect') + to.should_exit = from.get_value_or_null('-gexit') + to.should_exit_on_success = from.get_value_or_null('-gexit_on_success') + to.should_maximize = from.get_value_or_null('-gmaximize') + to.suffix = from.get_value_or_null('-gsuffix') + to.tests = from.get_value_or_null('-gtest') + to.unit_test_name = from.get_value_or_null('-gunit_test_name') + to.wait_log_delay = from.get_value_or_null('-gwait_log_delay') + + to.background_color = from.get_value_or_null('-gbackground_color') + to.font_color = from.get_value_or_null('-gfont_color') + to.font_name = from.get_value_or_null('-gfont_name') + to.font_size = from.get_value_or_null('-gfont_size') + to.paint_after = from.get_value_or_null('-gpaint_after') + + to.junit_xml_file = from.get_value_or_null('-gjunit_xml_file') + to.junit_xml_timestamp = from.get_value_or_null('-gjunit_xml_timestamp') + + to.failure_error_types = from.get_value_or_null('-gfailure_error_types') + to.no_error_tracking = from.get_value_or_null('-gno_error_tracking') + to.raie = from.get_value_or_null('-graie') + + + +func _print_gutconfigs(values): + var header = """Here is a sample of a full .gutconfig.json file. +You do not need to specify all values in your own file. The values supplied in +this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample +option. Option priority is: command-line, .gutconfig, default).""" + print("\n", header.replace("\n", ' '), "\n") + var resolved = values + + # remove_at some options that don't make sense to be in config + resolved.erase("config_file") + resolved.erase("show_help") + + print(JSON.stringify(resolved, ' ')) + + for key in resolved: + resolved[key] = null + + print("\n\nAnd here's an empty config for you fill in what you want.") + print(JSON.stringify(resolved, ' ')) + + +func _run_tests(opt_resolver): + _final_opts = opt_resolver.get_resolved_values(); + _gut_config.options = _final_opts + + var runner = GutRunner.instantiate() + runner.set_gut_config(_gut_config) + get_tree().root.add_child(runner) + + if(opt_resolver.cmd_opts.raie): + runner.run_from_editor() + else: + runner.run_tests() + + +var update_detector = null +func _check_for_update(): + print(str("Checking for update for GUT ", GutUtils.version_numbers.gut_version)) + update_detector = GutUtils.UpdateDetector.new() + add_child(update_detector) + await update_detector.check_for_update_with_fetch() + print(update_detector.get_update_string()) + + +# parse options and run Gut +func main(): + var opt_resolver = GutCliOptionResolver.new() + opt_resolver.set_base_opts(_gut_config.default_options) + + var cli_opts = setup_options(_gut_config.default_options, _gut_config.valid_fonts) + + cli_opts.parse() + var all_options_valid = cli_opts.unused.size() == 0 + extract_command_line_options(cli_opts, opt_resolver.cmd_opts) + + var config_path = opt_resolver.get_value('config_file') + var load_result = 1 + # Checking for an empty config path allows us to not use a config file via + # the -gconfig_file option since using "-gconfig_file=" or -gconfig_file=''" + # will result in an empty string. + if(config_path != ''): + load_result = _gut_config.load_options_no_defaults(config_path) + + # SHORTCIRCUIT + if(!all_options_valid): + print('Unknown arguments: ', cli_opts.unused) + get_tree().quit(1) + elif(load_result == -1): + print('Invalid gutconfig ', load_result) + get_tree().quit(1) + else: + opt_resolver.config_opts = _gut_config.options + + if(cli_opts.get_value('-gh')): + print(GutUtils.version_numbers.get_version_text()) + cli_opts.print_help() + get_tree().quit(0) + elif(cli_opts.get_value('-gpo')): + print('All config options and where they are specified. ' + + 'The "final" value shows which value will actually be used ' + + 'based on order of precedence (default < .gutconfig < cmd line).' + "\n") + print(opt_resolver.to_s_verbose()) + get_tree().quit(0) + elif(cli_opts.get_value('-gprint_gutconfig_sample')): + _print_gutconfigs(opt_resolver.get_resolved_values()) + get_tree().quit(0) + elif(cli_opts.get_value('-gcheck_update')): + await _check_for_update() + get_tree().quit(0) + else: + _run_tests(opt_resolver) + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/cli/gut_cli.gd.uid b/addons/gut/cli/gut_cli.gd.uid new file mode 100644 index 0000000..d57070b --- /dev/null +++ b/addons/gut/cli/gut_cli.gd.uid @@ -0,0 +1 @@ +uid://bhuudqinp4bth diff --git a/addons/gut/cli/optparse.gd b/addons/gut/cli/optparse.gd new file mode 100644 index 0000000..48ee9cc --- /dev/null +++ b/addons/gut/cli/optparse.gd @@ -0,0 +1,678 @@ +## Parses command line arguments, as one might expect. +## +## Parses command line arguments with a bunch of options including generating +## text that displays all the arguments your script accepts. This +## is included in the GUT ClassRef since it might be usable by others and is +## portable (everything it needs is in this one file). +## [br] +## This does alot, if you want to see it in action have a look at +## [url=https://github.com/bitwes/Gut/blob/main/scratch/optparse_example.gd]scratch/optparse_example.gd[/url] +## [codeblock lang=text] +## +## Godot Argument Lists +## ------------------------- +## There are two sets of command line arguments that Godot populates: +## OS.get_cmdline_args +## OS.get_cmdline_user_args. +## +## OS.get_cmdline_args contains any arguments that are not used by the engine +## itself. This means options like --help and -d will never appear in this list +## since these are used by the engine. The one exception is the -s option which +## is always included as the first entry and the script path as the second. +## Optparse ignores these values for argument processing but can be accessed +## with my_optparse.options.script_option. This list does not contain any +## arguments that appear in OS.get_cmdline_user_args. +## +## OS.get_cmdline_user_args contains any arguments that appear on the command +## line AFTER " -- " or " ++ ". This list CAN contain options that the engine +## would otherwise use, and are ignored completely by the engine. +## +## The parse method, by default, includes arguments from OS.get_cmdline_args and +## OS.get_cmdline_user_args. You can optionally pass one of these to the parse +## method to limit which arguments are parsed. You can also conjure up your own +## array of arguments and pass that to parse. +## +## See Godot's documentation for get_cmdline_args and get_cmdline_user_args for +## more information. +## +## +## Adding Options +## -------------- +## Use the following to add options to be parsed. These methods return the +## created Option instance. See that class above for more info. You can use +## the returned instance to get values, or use get_value/get_value_or_null. +## add("--name", "default", "Description goes here") +## add(["--name", "--aliases"], "default", "Description goes here") +## add_required(["--name", "--aliases"], "default", "Description goes here") +## add_positional("--name", "default", "Description goes here") +## add_positional_required("--name", "default", "Description goes here") +## +## get_value will return the value of the option or the default if it was not +## set. get_value_or_null will return the value of the option or null if it was +## not set. +## +## The Datatype for an option is determined from the default value supplied to +## the various add methods. Supported types are +## String +## Int +## Float +## Array of strings +## Boolean +## +## +## Value Parsing +## ------------- +## optparse uses option_name_prefix to differentiate between option names and +## values. Any argument that starts with this value will be treated as an +## argument name. The default is "-". Set this before calling parse if you want +## to change it. +## +## Values for options can be supplied on the command line with or without an "=": +## option=value # no space around "=" +## option value # a space between option and value w/o = +## There is no way to escape "=" at this time. +## +## Array options can be specified multiple times and/or set from a comma delimited +## list. +## -gdir=a,b +## -gdir c,d +## -gdir e +## Results in -gdir equaling [a, b, c, d, e]. There is no way to escape commas +## at this time. +## +## To specify an empty list via the command line follow the option with an equal +## sign +## -gdir= +## +## Boolean options will have thier value set to !default when they are supplied +## on the command line. Boolean options cannot have a value on the command line. +## They are either supplied or not. +## +## If a value is not an array and is specified multiple times on the command line +## then the last entry will be used as the value. +## +## Positional argument values are parsed after all named arguments are parsed. +## This means that other options can appear before, between, and after positional +## arguments. +## --foo=bar positional_0_value --disabled --bar foo positional_1_value --a_flag +## +## Anything that is not used by named or positional arguments will appear in the +## unused property. You can use this to detect unrecognized arguments or treat +## everything else provided as a list of things, or whatever you want. You can +## use is_option on the elements of unused (or whatever you want really) to see +## if optparse would treat it as an option name. +## +## Use get_missing_required_options to get an array of Option with all required +## options that were not found when parsing. +## +## The parsed_args property holds the list of arguments that were parsed. +## +## +## Help Generation +## --------------- +## You can call get_help to generate help text, or you can just call print_help +## and this will print it for you. +## +## Set the banner property to any text you want to appear before the usage and +## options sections. +## +## Options are printed in the order they are added. You can add a heading for +## different options sections with add_heading. +## add("--asdf", 1, "This will have no heading") +## add_heading("foo") +## add("--foo", false, "This will have the foo heading") +## add("--another_foo", 1.5, "This too.") +## add_heading("This is after foo") +## add("--bar", true, "You probably get it by now.") +## +## If you include "[default]" in the description of a option, then the help will +## substitue it with the default value. +## [/codeblock] + + +#------------------------------------------------------------------------------- +# Holds all the properties of a command line option +# +# value will return the default when it has not been set. +#------------------------------------------------------------------------------- +class OptParseOption: + var _has_been_set = false + var _value = null + # REMEMBER that when this option is an array, you have to set the value + # before you alter the contents of the array (append etc) or has_been_set + # will return false and it might not be used right. For example + # get_value_or_null will return null when you've actually changed the value. + var value = _value: + get: + return _value + + set(val): + _has_been_set = true + _value = val + + var option_name = '' + var default = null + var description = '' + var required = false + var aliases: Array[String] = [] + var show_in_help = true + + + func _init(name,default_value,desc=''): + option_name = name + default = default_value + description = desc + _value = default + + + func wrap_text(text, left_indent, max_length, wiggle_room=15): + var line_indent = str("\n", " ".repeat(left_indent + 1)) + var wrapped = '' + var position = 0 + var split_length = max_length + while(position < text.length()): + if(position > 0): + wrapped += line_indent + + var split_by = split_length + if(position + split_by + wiggle_room >= text.length()): + split_by = text.length() - position + else: + var min_space = text.rfind(' ', position + split_length) + var max_space = text.find(' ', position + split_length) + if(max_space <= position + split_length + wiggle_room): + split_by = max_space - position + else: + split_by = min_space - position + + wrapped += text.substr(position, split_by).lstrip(' ') + + if(position == 0): + split_length = max_length - left_indent + + position += split_by + + + return wrapped + + + + func to_s(min_space=0, wrap_length=100): + var line_indent = str("\n", " ".repeat(min_space + 1)) + var subbed_desc = description + if not aliases.is_empty(): + subbed_desc += "\naliases: " + ", ".join(aliases) + subbed_desc = subbed_desc.replace('[default]', str(default)) + subbed_desc = subbed_desc.replace("\n", line_indent) + + var final = str(option_name.rpad(min_space), ' ', subbed_desc) + if(wrap_length != -1): + final = wrap_text(final, min_space, wrap_length) + + return final + + + func has_been_set(): + return _has_been_set + + + + +#------------------------------------------------------------------------------- +# A struct for organizing options by a heading +#------------------------------------------------------------------------------- +class OptParseOptionHeading: + var options = [] + var display = 'default' + + + + +#------------------------------------------------------------------------------- +# Organizes options by order, heading, position. Also responsible for all +# help related text generation. +#------------------------------------------------------------------------------- +class OptParseOptions: + var options = [] + var positional = [] + var default_heading = OptParseOptionHeading.new() + var script_option = OptParseOption.new('-s', '?', 'script option provided by Godot') + + var _options_by_name = {"--script": script_option, "-s": script_option} + var _options_by_heading = [default_heading] + var _cur_heading = default_heading + + + func add_heading(display): + var heading = OptParseOptionHeading.new() + heading.display = display + _cur_heading = heading + _options_by_heading.append(heading) + + + func add(option, aliases=null): + options.append(option) + _options_by_name[option.option_name] = option + _cur_heading.options.append(option) + + if aliases != null: + for a in aliases: + _options_by_name[a] = option + option.aliases.assign(aliases) + + + func add_positional(option): + positional.append(option) + _options_by_name[option.option_name] = option + + + func get_by_name(option_name): + var found_param = null + if(_options_by_name.has(option_name)): + found_param = _options_by_name[option_name] + + return found_param + + + func get_help_text(): + var longest = 0 + var text = "" + for i in range(options.size()): + if(options[i].option_name.length() > longest): + longest = options[i].option_name.length() + + for heading in _options_by_heading: + if(heading != default_heading): + text += str("\n", heading.display, "\n") + for option in heading.options: + if(option.show_in_help): + text += str(' ', option.to_s(longest + 2).replace("\n", "\n "), "\n") + + return text + + + func get_option_value_text(): + var text = "" + var i = 0 + for option in positional: + text += str(i, '. ', option.option_name, ' = ', option.value) + + if(!option.has_been_set()): + text += " (default)" + text += "\n" + i += 1 + + for option in options: + text += str(option.option_name, ' = ', option.value) + + if(!option.has_been_set()): + text += " (default)" + text += "\n" + return text + + + func print_option_values(): + print(get_option_value_text()) + + + func get_missing_required_options(): + var to_return = [] + for opt in options: + if(opt.required and !opt.has_been_set()): + to_return.append(opt) + + for opt in positional: + if(opt.required and !opt.has_been_set()): + to_return.append(opt) + + return to_return + + + func get_usage_text(): + var pos_text = "" + for opt in positional: + pos_text += str("[", opt.description, "] ") + + if(pos_text != ""): + pos_text += " [opts] " + + return " -s " + script_option.value + " [opts] " + pos_text + + + + +#------------------------------------------------------------------------------- +# +# optarse +# +#------------------------------------------------------------------------------- +## @ignore +var options := OptParseOptions.new() +## Set the banner property to any text you want to appear before the usage and +## options sections when printing the options help. +var banner := '' +## optparse uses option_name_prefix to differentiate between option names and +## values. Any argument that starts with this value will be treated as an +## argument name. The default is "-". Set this before calling parse if you want +## to change it. +var option_name_prefix := '-' +## @ignore +var unused = [] +## @ignore +var parsed_args = [] +## @ignore +var values: Dictionary = {} + + +func _populate_values_dictionary(): + for entry in options.options: + var value_key = entry.option_name.lstrip('-') + values[value_key] = entry.value + + for entry in options.positional: + var value_key = entry.option_name.lstrip('-') + values[value_key] = entry.value + + +func _convert_value_to_array(raw_value): + var split = raw_value.split(',') + # This is what an empty set looks like from the command line. If we do + # not do this then we will always get back [''] which is not what it + # shoudl be. + if(split.size() == 1 and split[0] == ''): + split = [] + return split + +# REMEMBER raw_value not used for bools. +func _set_option_value(option, raw_value): + var t = typeof(option.default) + # only set values that were specified at the command line so that + # we can punch through default and config values correctly later. + # Without this check, you can't tell the difference between the + # defaults and what was specified, so you can't punch through + # higher level options. + if(t == TYPE_INT): + option.value = int(raw_value) + elif(t == TYPE_STRING): + option.value = str(raw_value) + elif(t == TYPE_ARRAY): + var values = _convert_value_to_array(raw_value) + if(!option.has_been_set()): + option.value = [] + option.value.append_array(values) + elif(t == TYPE_BOOL): + option.value = !option.default + elif(t == TYPE_FLOAT): + option.value = float(raw_value) + elif(t == TYPE_NIL): + print(option.option_name + ' cannot be processed, it has a nil datatype') + else: + print(option.option_name + ' cannot be processed, it has unknown datatype:' + str(t)) + + +func _parse_command_line_arguments(args): + var parsed_opts = args.duplicate() + var i = 0 + var positional_index = 0 + + while i < parsed_opts.size(): + var opt = '' + var value = '' + var entry = parsed_opts[i] + + if(is_option(entry)): + if(entry.find('=') != -1): + var parts = entry.split('=') + opt = parts[0] + value = parts[1] + var the_option = options.get_by_name(opt) + if(the_option != null): + parsed_opts.remove_at(i) + _set_option_value(the_option, value) + else: + i += 1 + else: + var the_option = options.get_by_name(entry) + if(the_option != null): + parsed_opts.remove_at(i) + if(typeof(the_option.default) == TYPE_BOOL): + _set_option_value(the_option, null) + elif(i < parsed_opts.size() and !is_option(parsed_opts[i])): + value = parsed_opts[i] + parsed_opts.remove_at(i) + _set_option_value(the_option, value) + else: + i += 1 + else: + if(positional_index < options.positional.size()): + _set_option_value(options.positional[positional_index], entry) + parsed_opts.remove_at(i) + positional_index += 1 + else: + i += 1 + + # this is the leftovers that were not extracted. + return parsed_opts + + +## Test if something is an existing argument. If [code]str(arg)[/code] begins +## with the [member option_name_prefix], it will considered true, +## otherwise it will be considered false. +func is_option(arg) -> bool: + return str(arg).begins_with(option_name_prefix) + + +## Adds a command line option. +## If [param op_names] is a String, this is set as the argument's name. +## If [param op_names] is an Array of Strings, all elements of the array +## will be aliases for the same argument and will be treated as such during +## parsing. +## [param default] is the default value the option will be set to if it is not +## explicitly set during parsing. +## [param desc] is a human readable text description of the option. +## If the option is successfully added, the Option object will be returned. +## If the option is not successfully added (e.g. a name collision with another +## option occurs), an error message will be printed and [code]null[/code] +## will be returned. +func add(op_names, default, desc: String) -> OptParseOption: + var op_name: String + var aliases: Array[String] = [] + var new_op: OptParseOption = null + + if(typeof(op_names) == TYPE_STRING): + op_name = op_names + else: + op_name = op_names[0] + aliases.assign(op_names.slice(1)) + + var bad_alias: int = aliases.map( + func (a: String) -> bool: return options.get_by_name(a) != null + ).find(true) + + if(options.get_by_name(op_name) != null): + push_error(str('Option [', op_name, '] already exists.')) + elif bad_alias != -1: + push_error(str('Option [', aliases[bad_alias], '] already exists.')) + else: + new_op = OptParseOption.new(op_name, default, desc) + options.add(new_op, aliases) + + return new_op + + +## Adds a required command line option. +## Required options that have not been set may be collected after parsing +## by calling [method get_missing_required_options]. +## If [param op_names] is a String, this is set as the argument's name. +## If [param op_names] is an Array of Strings, all elements of the array +## will be aliases for the same argument and will be treated as such during +## parsing. +## [param default] is the default value the option will be set to if it is not +## explicitly set during parsing. +## [param desc] is a human readable text description of the option. +## If the option is successfully added, the Option object will be returned. +## If the option is not successfully added (e.g. a name collision with another +## option occurs), an error message will be printed and [code]null[/code] +## will be returned. +func add_required(op_names, default, desc: String) -> OptParseOption: + var op := add(op_names, default, desc) + if(op != null): + op.required = true + return op + + +## Adds a positional command line option. +## Positional options are parsed by their position in the list of arguments +## are are not assigned by name by the user. +## If [param op_name] is a String, this is set as the argument's name. +## If [param op_name] is an Array of Strings, all elements of the array +## will be aliases for the same argument and will be treated as such during +## parsing. +## [param default] is the default value the option will be set to if it is not +## explicitly set during parsing. +## [param desc] is a human readable text description of the option. +## If the option is successfully added, the Option object will be returned. +## If the option is not successfully added (e.g. a name collision with another +## option occurs), an error message will be printed and [code]null[/code] +## will be returned. +func add_positional(op_name, default, desc: String) -> OptParseOption: + var new_op = null + if(options.get_by_name(op_name) != null): + push_error(str('Positional option [', op_name, '] already exists.')) + else: + new_op = OptParseOption.new(op_name, default, desc) + options.add_positional(new_op) + return new_op + + +## Adds a required positional command line option. +## If [param op_name] is a String, this is set as the argument's name. +## Required options that have not been set may be collected after parsing +## by calling [method get_missing_required_options]. +## Positional options are parsed by their position in the list of arguments +## are are not assigned by name by the user. +## If [param op_name] is an Array of Strings, all elements of the array +## will be aliases for the same argument and will be treated as such during +## parsing. +## [param default] is the default value the option will be set to if it is not +## explicitly set during parsing. +## [param desc] is a human readable text description of the option. +## If the option is successfully added, the Option object will be returned. +## If the option is not successfully added (e.g. a name collision with another +## option occurs), an error message will be printed and [code]null[/code] +## will be returned. +func add_positional_required(op_name, default, desc: String) -> OptParseOption: + var op = add_positional(op_name, default, desc) + if(op != null): + op.required = true + return op + + +## Headings are used to separate logical groups of command line options +## when printing out options from the help menu. +## Headings are printed out between option descriptions in the order +## that [method add_heading] was called. +func add_heading(display_text: String) -> void: + options.add_heading(display_text) + + +## Gets the value assigned to an option after parsing. +## [param name] can be the name of the option or an alias of it. +## [param name] specifies the option whose value you wish to query. +## If the option exists, the value assigned to it during parsing is returned. +## Otherwise, an error message is printed and [code]null[/code] is returned. +func get_value(name: String): + var found_param: OptParseOption = options.get_by_name(name) + + if(found_param != null): + return found_param.value + else: + push_error("COULD NOT FIND OPTION " + name) + return null + + +## Gets the value assigned to an option after parsing, +## returning null if the option was not assigned instead of its default value. +## [param name] specifies the option whose value you wish to query. +## This can be useful when providing an order of precedence to your values. +## For example if +## [codeblock] +## default value < config file < command line +## [/codeblock] +## then you do not want to get the default value for a command line option or +## it will overwrite the value in a config file. +func get_value_or_null(name: String): + var found_param: OptParseOption = options.get_by_name(name) + + if(found_param != null and found_param.has_been_set()): + return found_param.value + else: + return null + + +## Returns the help text for all defined options. +func get_help() -> String: + var sep := '---------------------------------------------------------' + + var text := str(sep, "\n", banner, "\n\n") + text += "Usage\n-----------\n" + text += " " + options.get_usage_text() + "\n\n" + text += "\nOptions\n-----------\n" + text += options.get_help_text() + text += str(sep, "\n") + return text + + +## Prints out the help text for all defined options. +func print_help() -> void: + print(get_help()) + + +## Parses a string for all options that have been set in this optparse. +## if [param cli_args] is passed as a String, then it is parsed. +## Otherwise if [param cli_args] is null, +## aruments passed to the Godot engine at startup are parsed. +## See the explanation at the top of addons/gut/cli/optparse.gd to understand +## which arguments this will have access to. +func parse(cli_args=null) -> void: + parsed_args = cli_args + + if(parsed_args == null): + parsed_args = OS.get_cmdline_args() + parsed_args.append_array(OS.get_cmdline_user_args()) + + unused = _parse_command_line_arguments(parsed_args) + _populate_values_dictionary() + + +## Get all options that were required and were not set during parsing. +## The return value is an Array of Options. +func get_missing_required_options() -> Array: + return options.get_missing_required_options() + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## \ No newline at end of file diff --git a/addons/gut/cli/optparse.gd.uid b/addons/gut/cli/optparse.gd.uid new file mode 100644 index 0000000..efcf3e1 --- /dev/null +++ b/addons/gut/cli/optparse.gd.uid @@ -0,0 +1 @@ +uid://c8m4fojwln6bq diff --git a/addons/gut/collected_script.gd b/addons/gut/collected_script.gd new file mode 100644 index 0000000..7b7324e --- /dev/null +++ b/addons/gut/collected_script.gd @@ -0,0 +1,208 @@ +# ------------------------------------------------------------------------------ +# This holds all the meta information for a test script. It contains the +# name of the inner class and an array of CollectedTests. This does not parse +# anything, it just holds the data about parsed scripts and tests. The +# TestCollector is responsible for populating this object. +# +# This class also facilitates all the exporting and importing of tests. +# ------------------------------------------------------------------------------ +var CollectedTest = GutUtils.CollectedTest + +var _lgr = null + +# One entry per test found in the script. Added externally by TestCollector +var tests = [] +# One entry for before_all and after_all (maybe add before_each and after_each). +# These are added by Gut when running before_all and after_all for the script. +var setup_teardown_tests = [] +var inner_class_name:StringName +var path:String + + +# Set externally by test_collector after it can verify that the script was +# actually loaded. This could probably be changed to just hold the GutTest +# script that was loaded, cutting down on complexity elsewhere. +var is_loaded = false + +# Set by Gut when it decides that a script should be skipped. +# Right now this is whenever the script has the variable skip_script declared. +# the value of skip_script is put into skip_reason. +var was_skipped = false +var skip_reason = '' +var was_run = false + + +var name = '' : + get: return path + set(val):pass + + +func _init(logger=null): + _lgr = logger + + +func get_new(): + var inst = load_script().new() + inst.collected_script = self + return inst + + +func load_script(): + var to_return = load(path) + + if(inner_class_name != null and inner_class_name != ''): + # If we wanted to do inner classes in inner classses + # then this would have to become some kind of loop or recursive + # call to go all the way down the chain or this class would + # have to change to hold onto the loaded class instead of + # just path information. + to_return = to_return.get(inner_class_name) + + return to_return + +# script.gd.InnerClass +func get_filename_and_inner(): + var to_return = get_filename() + if(inner_class_name != ''): + to_return += '.' + String(inner_class_name) + return to_return + + +# res://foo/bar.gd.FooBar +func get_full_name(): + var to_return = path + if(inner_class_name != ''): + to_return += '.' + String(inner_class_name) + return to_return + + +func get_filename(): + return path.get_file() + + +func has_inner_class(): + return inner_class_name != '' + + +# Note: although this no longer needs to export the inner_class names since +# they are pulled from metadata now, it is easier to leave that in +# so we don't have to cut the export down to unique script names. +func export_to(config_file, section): + config_file.set_value(section, 'path', path) + config_file.set_value(section, 'inner_class', inner_class_name) + var names = [] + for i in range(tests.size()): + names.append(tests[i].name) + config_file.set_value(section, 'tests', names) + + +func _remap_path(source_path): + var to_return = source_path + if(!FileAccess.file_exists(source_path)): + _lgr.debug('Checking for remap for: ' + source_path) + var remap_path = source_path.get_basename() + '.gd.remap' + if(FileAccess.file_exists(remap_path)): + var cf = ConfigFile.new() + cf.load(remap_path) + to_return = cf.get_value('remap', 'path') + else: + _lgr.warn('Could not find remap file ' + remap_path) + return to_return + + +func import_from(config_file, section): + path = config_file.get_value(section, 'path') + path = _remap_path(path) + # Null is an acceptable value, but you can't pass null as a default to + # get_value since it thinks you didn't send a default...then it spits + # out red text. This works around that. + var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') + if(inner_name != 'Placeholder'): + inner_class_name = inner_name + else: # just being explicit + inner_class_name = StringName("") + + +func get_test_named(test_name): + return GutUtils.search_array(tests, 'name', test_name) + + +func get_ran_test_count(): + var count = 0 + for t in tests: + if(t.was_run): + count += 1 + return count + + +func get_assert_count(): + var count = 0 + for t in tests: + count += t.pass_texts.size() + count += t.fail_texts.size() + for t in setup_teardown_tests: + count += t.pass_texts.size() + count += t.fail_texts.size() + return count + + +func get_pass_count(): + var count = 0 + for t in tests: + count += t.pass_texts.size() + for t in setup_teardown_tests: + count += t.pass_texts.size() + return count + + +func get_fail_count(): + var count = 0 + for t in tests: + count += t.fail_texts.size() + for t in setup_teardown_tests: + count += t.fail_texts.size() + return count + + +func get_pending_count(): + var count = 0 + for t in tests: + count += t.pending_texts.size() + return count + + +func get_passing_test_count(): + var count = 0 + for t in tests: + if(t.is_passing()): + count += 1 + return count + + +func get_failing_test_count(): + var count = 0 + for t in tests: + if(t.is_failing()): + count += 1 + return count + + +func get_risky_count(): + var count = 0 + if(was_skipped): + count = 1 + else: + for t in tests: + if(t.is_risky()): + count += 1 + return count + + +func to_s(): + var to_return = path + if(inner_class_name != null): + to_return += str('.', inner_class_name) + to_return += "\n" + for i in range(tests.size()): + to_return += str(' ', tests[i].to_s()) + return to_return diff --git a/addons/gut/collected_script.gd.uid b/addons/gut/collected_script.gd.uid new file mode 100644 index 0000000..ae2a94a --- /dev/null +++ b/addons/gut/collected_script.gd.uid @@ -0,0 +1 @@ +uid://bjjcnr1oqvag6 diff --git a/addons/gut/collected_test.gd b/addons/gut/collected_test.gd new file mode 100644 index 0000000..3e30a13 --- /dev/null +++ b/addons/gut/collected_test.gd @@ -0,0 +1,120 @@ +# ------------------------------------------------------------------------------ +# Used to keep track of info about each test ran. +# ------------------------------------------------------------------------------ +# the name of the function +var name = "" + +# flag to know if the name has been printed yet. Used by the logger. +var has_printed_name = false + +# the number of arguments the method has +var arg_count = 0 + +# the time it took to execute the test in seconds +var time_taken : float = 0 + +# The number of asserts in the test. Converted to a property for backwards +# compatibility. This now reflects the text sizes instead of being a value +# that can be altered externally. +var assert_count = 0 : + get: return pass_texts.size() + fail_texts.size() + set(val): pass + +# Converted to propety for backwards compatibility. This now cannot be set +# externally +var pending = false : + get: return is_pending() + set(val): pass + +# the line number when the test fails +var line_number = -1 + +# Set internally by Gut using whatever reason Gut wants to use to set this. +# Gut will skip these marked true and the test will be listed as risky. +var should_skip = false # -- Currently not used by GUT don't believe ^ + +var pass_texts = [] +var fail_texts = [] +var pending_texts = [] +var orphans = 0 + +var was_run = false + +var collected_script : WeakRef = null + + +func did_pass(): + return is_passing() + + +func add_fail(fail_text): + fail_texts.append(fail_text) + + +func add_pending(pending_text): + pending_texts.append(pending_text) + + +func add_pass(passing_text): + pass_texts.append(passing_text) + + +# must have passed an assert and not have any other status to be passing +func is_passing(): + return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0 + + +# failing takes precedence over everything else, so any failures makes the +# test a failure. +func is_failing(): + return fail_texts.size() > 0 + + +# test is only pending if pending was called and the test is not failing. +func is_pending(): + return pending_texts.size() > 0 and fail_texts.size() == 0 + + +func is_risky(): + return should_skip or (was_run and !did_something()) + + +func did_something(): + return is_passing() or is_failing() or is_pending() + + +func get_status_text(): + var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS + + if(should_skip): + to_return = GutUtils.TEST_STATUSES.SKIPPED + elif(!was_run): + to_return = GutUtils.TEST_STATUSES.NOT_RUN + elif(pending_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.PENDING + elif(fail_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.FAILED + elif(pass_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.PASSED + + return to_return + + +# Deprecated +func get_status(): + return get_status_text() + + +func to_s(): + var pad = ' ' + var to_return = str(name, "[", get_status_text(), "]\n") + + for i in range(fail_texts.size()): + to_return += str(pad, 'Fail: ', fail_texts[i]) + for i in range(pending_texts.size()): + to_return += str(pad, 'Pending: ', pending_texts[i], "\n") + for i in range(pass_texts.size()): + to_return += str(pad, 'Pass: ', pass_texts[i], "\n") + return to_return + + diff --git a/addons/gut/collected_test.gd.uid b/addons/gut/collected_test.gd.uid new file mode 100644 index 0000000..97778ed --- /dev/null +++ b/addons/gut/collected_test.gd.uid @@ -0,0 +1 @@ +uid://cl854f1m26a2a diff --git a/addons/gut/comparator.gd b/addons/gut/comparator.gd new file mode 100644 index 0000000..8510d67 --- /dev/null +++ b/addons/gut/comparator.gd @@ -0,0 +1,125 @@ +var _strutils = GutUtils.Strutils.new() +var _max_length = 100 +var _should_compare_int_to_float = true + +const MISSING = '|__missing__gut__compare__value__|' + + +func _cannot_compare_text(v1, v2): + return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ', + _strutils.types[typeof(v2)], '.') + + +func _make_missing_string(text): + return '' + + +func _create_missing_result(v1, v2, text): + var to_return = null + var v1_str = format_value(v1) + var v2_str = format_value(v2) + + if(typeof(v1) == TYPE_STRING and v1 == MISSING): + v1_str = _make_missing_string(text) + to_return = GutUtils.CompareResult.new() + elif(typeof(v2) == TYPE_STRING and v2 == MISSING): + v2_str = _make_missing_string(text) + to_return = GutUtils.CompareResult.new() + + if(to_return != null): + to_return.summary = str(v1_str, ' != ', v2_str) + to_return.are_equal = false + + return to_return + + +func simple(v1, v2, missing_string=''): + var missing_result = _create_missing_result(v1, v2, missing_string) + if(missing_result != null): + return missing_result + + var result = GutUtils.CompareResult.new() + var cmp_str = null + var extra = '' + + var tv1 = typeof(v1) + var tv2 = typeof(v2) + + # print(tv1, '::', tv2, ' ', _strutils.types[tv1], '::', _strutils.types[tv2]) + if(_should_compare_int_to_float and [TYPE_INT, TYPE_FLOAT].has(tv1) and [TYPE_INT, TYPE_FLOAT].has(tv2)): + result.are_equal = v1 == v2 + elif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)): + result.are_equal = v1 == v2 + elif(GutUtils.are_datatypes_same(v1, v2)): + result.are_equal = v1 == v2 + + if(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY): + var sub_result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) + result.summary = sub_result.get_short_summary() + if(!sub_result.are_equal): + extra = ".\n" + sub_result.get_short_summary() + else: + cmp_str = '!=' + result.are_equal = false + extra = str('. ', _cannot_compare_text(v1, v2)) + + cmp_str = get_compare_symbol(result.are_equal) + result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra) + + return result + + +func shallow(v1, v2): + var result = null + if(GutUtils.are_datatypes_same(v1, v2)): + if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): + result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) + else: + result = simple(v1, v2) + else: + result = simple(v1, v2) + + return result + + +func deep(v1, v2): + var result = null + + if(GutUtils.are_datatypes_same(v1, v2)): + if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): + result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) + else: + result = simple(v1, v2) + else: + result = simple(v1, v2) + + return result + + +func format_value(val, max_val_length=_max_length): + return _strutils.truncate_string(_strutils.type2str(val), max_val_length) + + +func compare(v1, v2, diff_type=GutUtils.DIFF.SIMPLE): + var result = null + if(diff_type == GutUtils.DIFF.SIMPLE): + result = simple(v1, v2) + elif(diff_type == GutUtils.DIFF.DEEP): + result = deep(v1, v2) + + return result + + +func get_should_compare_int_to_float(): + return _should_compare_int_to_float + + +func set_should_compare_int_to_float(should_compare_int_float): + _should_compare_int_to_float = should_compare_int_float + + +func get_compare_symbol(is_equal): + if(is_equal): + return '==' + else: + return '!=' diff --git a/addons/gut/comparator.gd.uid b/addons/gut/comparator.gd.uid new file mode 100644 index 0000000..10c63ff --- /dev/null +++ b/addons/gut/comparator.gd.uid @@ -0,0 +1 @@ +uid://bohry7fhscy7y diff --git a/addons/gut/compare_result.gd b/addons/gut/compare_result.gd new file mode 100644 index 0000000..1a8afb5 --- /dev/null +++ b/addons/gut/compare_result.gd @@ -0,0 +1,70 @@ +var _are_equal = false +var are_equal = false : + get: + return get_are_equal() + set(val): + set_are_equal(val) + +var _summary = null +var summary = null : + get: + return get_summary() + set(val): + set_summary(val) + +var _max_differences = 30 +var max_differences = 30 : + get: + return get_max_differences() + set(val): + set_max_differences(val) + +var _differences = {} +var differences : + get: + return get_differences() + set(val): + set_differences(val) + +func _block_set(which, val): + push_error(str('cannot set ', which, ', value [', val, '] ignored.')) + +func _to_string(): + return str(get_summary()) # could be null, gotta str it. + +func get_are_equal(): + return _are_equal + +func set_are_equal(r_eq): + _are_equal = r_eq + +func get_summary(): + return _summary + +func set_summary(smry): + _summary = smry + +func get_total_count(): + pass + +func get_different_count(): + pass + +func get_short_summary(): + return summary + +func get_max_differences(): + return _max_differences + +func set_max_differences(max_diff): + _max_differences = max_diff + +func get_differences(): + return _differences + +func set_differences(diffs): + _block_set('differences', diffs) + +func get_brackets(): + return null + diff --git a/addons/gut/compare_result.gd.uid b/addons/gut/compare_result.gd.uid new file mode 100644 index 0000000..aed99a4 --- /dev/null +++ b/addons/gut/compare_result.gd.uid @@ -0,0 +1 @@ +uid://cow1xqmqqvn4e diff --git a/addons/gut/diff_formatter.gd b/addons/gut/diff_formatter.gd new file mode 100644 index 0000000..05f898f --- /dev/null +++ b/addons/gut/diff_formatter.gd @@ -0,0 +1,63 @@ +var _strutils = GutUtils.Strutils.new() +const INDENT = ' ' +var _max_to_display = 30 +const ABSOLUTE_MAX_DISPLAYED = 10000 +const UNLIMITED = -1 + + +func _single_diff(diff, depth=0): + var to_return = "" + var brackets = diff.get_brackets() + + if(brackets != null and !diff.are_equal): + to_return = '' + to_return += str(brackets.open, "\n", + _strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n", + brackets.close) + else: + to_return = str(diff) + + return to_return + + +func make_it(diff): + var to_return = '' + if(diff.are_equal): + to_return = diff.summary + else: + if(_max_to_display == ABSOLUTE_MAX_DISPLAYED): + to_return = str(diff.get_value_1(), ' != ', diff.get_value_2()) + else: + to_return = diff.get_short_summary() + to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' ')) + return to_return + + +func differences_to_s(differences, depth=0): + var to_return = '' + var keys = differences.keys() + keys.sort() + var limit = min(_max_to_display, differences.size()) + + for i in range(limit): + var key = keys[i] + to_return += str(key, ": ", _single_diff(differences[key], depth)) + + if(i != limit -1): + to_return += "\n" + + if(differences.size() > _max_to_display): + to_return += str("\n\n... ", differences.size() - _max_to_display, " more.") + + return to_return + + +func get_max_to_display(): + return _max_to_display + + +func set_max_to_display(max_to_display): + _max_to_display = max_to_display + if(_max_to_display == UNLIMITED): + _max_to_display = ABSOLUTE_MAX_DISPLAYED + diff --git a/addons/gut/diff_formatter.gd.uid b/addons/gut/diff_formatter.gd.uid new file mode 100644 index 0000000..5662859 --- /dev/null +++ b/addons/gut/diff_formatter.gd.uid @@ -0,0 +1 @@ +uid://ch2km05phxacd diff --git a/addons/gut/diff_tool.gd b/addons/gut/diff_tool.gd new file mode 100644 index 0000000..d1b5091 --- /dev/null +++ b/addons/gut/diff_tool.gd @@ -0,0 +1,156 @@ +extends 'res://addons/gut/compare_result.gd' +const INDENT = ' ' +enum { + DEEP, + SIMPLE +} + +var _strutils = GutUtils.Strutils.new() +var _compare = GutUtils.Comparator.new() + +var _value_1 = null +var _value_2 = null +var _total_count = 0 +var _diff_type = null +var _brackets = null +var _valid = true +var _desc_things = 'somethings' + +# -------- comapre_result.gd "interface" --------------------- +func set_are_equal(val): + _block_set('are_equal', val) + +func get_are_equal(): + if(!_valid): + return null + else: + return differences.size() == 0 + + +func set_summary(val): + _block_set('summary', val) + +func get_summary(): + return summarize() + +func get_different_count(): + return differences.size() + +func get_total_count(): + return _total_count + +func get_short_summary(): + var text = str(_strutils.truncate_string(str(_value_1), 50), + ' ', _compare.get_compare_symbol(are_equal), ' ', + _strutils.truncate_string(str(_value_2), 50)) + if(!are_equal): + text += str(' ', get_different_count(), ' of ', get_total_count(), + ' ', _desc_things, ' do not match.') + return text + +func get_brackets(): + return _brackets +# -------- comapre_result.gd "interface" --------------------- + + +func _invalidate(): + _valid = false + differences = null + + +func _init(v1,v2,diff_type=DEEP): + _value_1 = v1 + _value_2 = v2 + _diff_type = diff_type + _compare.set_should_compare_int_to_float(false) + _find_differences(_value_1, _value_2) + + +func _find_differences(v1, v2): + if(GutUtils.are_datatypes_same(v1, v2)): + if(typeof(v1) == TYPE_ARRAY): + _brackets = {'open':'[', 'close':']'} + _desc_things = 'indexes' + _diff_array(v1, v2) + elif(typeof(v2) == TYPE_DICTIONARY): + _brackets = {'open':'{', 'close':'}'} + _desc_things = 'keys' + _diff_dictionary(v1, v2) + else: + _invalidate() + GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.') + else: + _invalidate() + GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.') + + +func _diff_array(a1, a2): + _total_count = max(a1.size(), a2.size()) + for i in range(a1.size()): + var result = null + if(i < a2.size()): + if(_diff_type == DEEP): + result = _compare.deep(a1[i], a2[i]) + else: + result = _compare.simple(a1[i], a2[i]) + else: + result = _compare.simple(a1[i], _compare.MISSING, 'index') + + if(!result.are_equal): + differences[i] = result + + if(a1.size() < a2.size()): + for i in range(a1.size(), a2.size()): + differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index') + + +func _diff_dictionary(d1, d2): + var d1_keys = d1.keys() + var d2_keys = d2.keys() + + # Process all the keys in d1 + _total_count += d1_keys.size() + for key in d1_keys: + if(!d2.has(key)): + differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key') + else: + d2_keys.remove_at(d2_keys.find(key)) + + var result = null + if(_diff_type == DEEP): + result = _compare.deep(d1[key], d2[key]) + else: + result = _compare.simple(d1[key], d2[key]) + + if(!result.are_equal): + differences[key] = result + + # Process all the keys in d2 that didn't exist in d1 + _total_count += d2_keys.size() + for i in range(d2_keys.size()): + differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key') + + +func summarize(): + var summary = '' + + if(are_equal): + summary = get_short_summary() + else: + var formatter = load('res://addons/gut/diff_formatter.gd').new() + formatter.set_max_to_display(max_differences) + summary = formatter.make_it(self) + + return summary + + +func get_diff_type(): + return _diff_type + + +func get_value_1(): + return _value_1 + + +func get_value_2(): + return _value_2 diff --git a/addons/gut/diff_tool.gd.uid b/addons/gut/diff_tool.gd.uid new file mode 100644 index 0000000..1e49604 --- /dev/null +++ b/addons/gut/diff_tool.gd.uid @@ -0,0 +1 @@ +uid://beoxokvl1hjs8 diff --git a/addons/gut/double_templates/double_data_template.txt b/addons/gut/double_templates/double_data_template.txt new file mode 100644 index 0000000..294c7df --- /dev/null +++ b/addons/gut/double_templates/double_data_template.txt @@ -0,0 +1,22 @@ +var __gutdbl_values = { + thepath = '{path}', + subpath = '{subpath}', + stubber = {stubber_id}, + spy = {spy_id}, + gut = {gut_id}, + singleton_name = '{singleton_name}', + singleton = {singleton_id}, + is_partial = {is_partial}, + doubled_methods = {doubled_methods}, +} +var __gutdbl = load('res://addons/gut/double_tools.gd').new(self) + +# Here so other things can check for a method to know if this is a double. +func __gutdbl_check_method__(): + pass + +# Cleanup called by GUT after tests have finished. Important for RefCounted +# objects. Nodes are freed, and won't have this method called on them. +func __gutdbl_done(): + __gutdbl = null + __gutdbl_values.clear() diff --git a/addons/gut/double_templates/function_template.txt b/addons/gut/double_templates/function_template.txt new file mode 100644 index 0000000..2a41eb3 --- /dev/null +++ b/addons/gut/double_templates/function_template.txt @@ -0,0 +1,9 @@ +{func_decleration} + if(__gutdbl == null): + {super_call} + else: + __gutdbl.spy_on('{method_name}', {param_array}) + if(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})): + {super_call} + else: + {return_it}await __gutdbl.handle_other_stubs('{method_name}', {param_array}) diff --git a/addons/gut/double_templates/init_template.txt b/addons/gut/double_templates/init_template.txt new file mode 100644 index 0000000..9b43474 --- /dev/null +++ b/addons/gut/double_templates/init_template.txt @@ -0,0 +1,4 @@ +{func_decleration}: + super({super_params}) + __gutdbl.spy_on('{method_name}', {param_array}) + diff --git a/addons/gut/double_templates/script_template.txt b/addons/gut/double_templates/script_template.txt new file mode 100644 index 0000000..be174ec --- /dev/null +++ b/addons/gut/double_templates/script_template.txt @@ -0,0 +1,13 @@ +# ############################################################################## +# Gut Doubled Script +# ############################################################################## +{extends} + +# ------------------------------------------------------------------------------ +# GUT stuff +# ------------------------------------------------------------------------------ +{double_data} + +# ------------------------------------------------------------------------------ +# Doubled Methods +# ------------------------------------------------------------------------------ diff --git a/addons/gut/double_templates/singleton_template.txt b/addons/gut/double_templates/singleton_template.txt new file mode 100644 index 0000000..df1c6c4 --- /dev/null +++ b/addons/gut/double_templates/singleton_template.txt @@ -0,0 +1,16 @@ +{extends} + +{constants} + +{signals} + +{properties} + +# ------------------------------------------------------------------------------ +# GUT stuff +# ------------------------------------------------------------------------------ +{double_data} + +# ------------------------------------------------------------------------------ +# Doubled Methods +# ------------------------------------------------------------------------------ diff --git a/addons/gut/double_tools.gd b/addons/gut/double_tools.gd new file mode 100644 index 0000000..148df65 --- /dev/null +++ b/addons/gut/double_tools.gd @@ -0,0 +1,83 @@ +var thepath = '' +var subpath = '' +var singleton_name = null +var is_partial = null + +var double_ref : WeakRef = null +var stubber_ref : WeakRef = null +var spy_ref : WeakRef = null +var gut_ref : WeakRef = null +var singleton_ref : WeakRef = null +var __gutdbl_values = {} + +const NO_DEFAULT_VALUE = '!__gut__no__default__value__!' +func _init(double = null): + if(double != null): + var values = double.__gutdbl_values + __gutdbl_values = double.__gutdbl_values + double_ref = weakref(double) + thepath = values.thepath + subpath = values.subpath + stubber_ref = weakref_from_id(values.stubber) + spy_ref = weakref_from_id(values.spy) + gut_ref = weakref_from_id(values.gut) + singleton_ref = weakref_from_id(values.singleton) + singleton_name = values.singleton_name + is_partial = values.is_partial + + if(gut_ref.get_ref() != null): + gut_ref.get_ref().get_autofree().add_free(double_ref.get_ref()) + + +func _get_stubbed_method_to_call(method_name, called_with): + var method = stubber_ref.get_ref().get_call_this(double_ref.get_ref(), method_name, called_with) + if(method != null): + method = method.bindv(called_with) + return method + return method + + +func weakref_from_id(inst_id): + if(inst_id == -1): + return weakref(null) + else: + return weakref(instance_from_id(inst_id)) + + +func is_stubbed_to_call_super(method_name, called_with): + if(stubber_ref.get_ref() != null): + return stubber_ref.get_ref().should_call_super(double_ref.get_ref(), method_name, called_with) + else: + return false + + +func handle_other_stubs(method_name, called_with): + if(stubber_ref.get_ref() == null): + return + + var method = _get_stubbed_method_to_call(method_name, called_with) + if(method != null): + return await method.call() + else: + return stubber_ref.get_ref().get_return(double_ref.get_ref(), method_name, called_with) + + +func spy_on(method_name, called_with): + if(spy_ref.get_ref() != null): + spy_ref.get_ref().add_call(double_ref.get_ref(), method_name, called_with) + + +func default_val(method_name, p_index): + if(stubber_ref.get_ref() == null): + return null + else: + var result = stubber_ref.get_ref().get_default_value(double_ref.get_ref(), method_name, p_index) + return result + + +func get_singleton(): + var to_return = singleton_ref.get_ref() + if(to_return == null): + push_error("Trying to get a singleton reference on a non-singleton double: ", + __gutdbl_values.singleton_name, "/", __gutdbl_values.singleton) + return to_return \ No newline at end of file diff --git a/addons/gut/double_tools.gd.uid b/addons/gut/double_tools.gd.uid new file mode 100644 index 0000000..0113fa6 --- /dev/null +++ b/addons/gut/double_tools.gd.uid @@ -0,0 +1 @@ +uid://tr4khoco1hef diff --git a/addons/gut/doubler.gd b/addons/gut/doubler.gd new file mode 100644 index 0000000..878b9e1 --- /dev/null +++ b/addons/gut/doubler.gd @@ -0,0 +1,392 @@ +extends RefCounted + + +static var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt') +static var _singleton_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/singleton_template.txt') +static var _double_data_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/double_data_template.txt') + +var _script_collector = GutUtils.ScriptCollector.new() +var _singleton_parser = GutUtils.SingletonParser.new() + +# used by tests for debugging purposes. +var print_source = false +var inner_class_registry = GutUtils.inner_class_registry + +# ############### +# Properties +# ############### +var _stubber = GutUtils.Stubber.new() +func get_stubber(): + return _stubber +func set_stubber(stubber): + _stubber = stubber + +var _lgr = GutUtils.get_logger() +func get_logger(): + return _lgr +func set_logger(logger): + _lgr = logger + _method_maker.set_logger(logger) + +var _spy = null +func get_spy(): + return _spy +func set_spy(spy): + _spy = spy + +var _gut = null +func get_gut(): + return _gut +func set_gut(gut): + _gut = gut + +var _strategy = null +func get_strategy(): + return _strategy +func set_strategy(strategy): + if(GutUtils.DOUBLE_STRATEGY.values().has(strategy)): + _strategy = strategy + else: + _lgr.error(str('doubler.gd: invalid double strategy ', strategy)) + +var _method_maker = GutUtils.MethodMaker.new() +func get_method_maker(): + return _method_maker + +var _ignored_methods = GutUtils.OneToMany.new() +func get_ignored_methods(): + return _ignored_methods + + +# ############### +# Private +# ############### +func _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY): + set_logger(GutUtils.get_logger()) + _strategy = strategy + + +func _notification(what: int) -> void: + if(what == NOTIFICATION_PREDELETE): + if(_stubber != null): + _stubber.clear() + +func _get_indented_line(indents, text): + var to_return = '' + for _i in range(indents): + to_return += "\t" + return str(to_return, text, "\n") + + +func _stub_to_call_super(parsed, method_name): + if(!parsed.get_method(method_name).is_eligible_for_doubling()): + return + + var params = null + if(parsed.is_native): + params = GutUtils.StubParams.new(parsed._native_class, method_name, parsed.subpath) + else: + params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath) + params.to_call_super() + params.is_script_default = true + _stubber.add_stub(params) + + +func _get_base_script_text(parsed, override_path, partial, included_methods): + var path = parsed.script_path + if(override_path != null): + path = override_path + + var stubber_id = -1 + if(_stubber != null): + stubber_id = _stubber.get_instance_id() + + var spy_id = -1 + if(_spy != null): + spy_id = _spy.get_instance_id() + + var gut_id = -1 + if(_gut != null): + gut_id = _gut.get_instance_id() + + var extends_text = parsed.get_extends_text() + var double_data_values = { + "path":path, + "subpath":GutUtils.nvl(parsed.subpath, ''), + "stubber_id":stubber_id, + "spy_id":spy_id, + "gut_id":gut_id, + "singleton_name":'', + "singleton_id":-1, + "is_partial":partial, + "doubled_methods":included_methods, + } + + var values = { + "extends":extends_text, + "double_data":_double_data_text.format(double_data_values), + } + + return _base_script_text.format(values) + + +func _get_singleton_text(parsed, included_methods, is_partial): + var stubber_id = -1 + if(_stubber != null): + stubber_id = _stubber.get_instance_id() + + var spy_id = -1 + if(_spy != null): + spy_id = _spy.get_instance_id() + + var gut_id = -1 + if(_gut != null): + gut_id = _gut.get_instance_id() + + var double_data_values = { + "path":'', + "subpath":'', + "stubber_id":stubber_id, + "spy_id":spy_id, + "gut_id":gut_id, + "singleton_name":parsed.singleton_name, + "singleton_id":parsed.singleton_id, + "is_partial":is_partial, + "doubled_methods":included_methods, + } + + var values = { + "extends":"extends RefCounted", + "double_data":_double_data_text.format(double_data_values), + "signals":parsed.get_all_signal_text(), + "constants":parsed.get_all_constants_text(), + "properties":parsed.get_all_properties_text() + } + + var src = _singleton_script_text.format(values) + return src + + +func _is_method_eligible_for_doubling(parsed_script, parsed_method): + return !parsed_method.is_accessor() and \ + parsed_method.is_eligible_for_doubling() and \ + !_ignored_methods.has(parsed_script.resource, parsed_method.meta.name) + + +# Disable the native_method_override setting so that doubles do not generate +# errors or warnings when doubling with INCLUDE_NATIVE or when a method has +# been added because of param_count stub. +func _create_script_no_warnings(src): + var prev_native_override_value = null + var native_method_override = 'debug/gdscript/warnings/native_method_override' + prev_native_override_value = ProjectSettings.get_setting(native_method_override) + ProjectSettings.set_setting(native_method_override, 0) + + var DblClass = GutUtils.create_script_from_source(src) + + ProjectSettings.set_setting(native_method_override, prev_native_override_value) + return DblClass + + +func _create_double(parsed, strategy, override_path, partial): + var dbl_src = "" + var included_methods = [] + + for method in parsed.get_local_methods(): + if(_is_method_eligible_for_doubling(parsed, method)): + included_methods.append(method.meta.name) + dbl_src += _method_maker.get_function_text(method) + + if(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE): + for method in parsed.get_super_methods(): + if(_is_method_eligible_for_doubling(parsed, method)): + included_methods.append(method.meta.name) + _stub_to_call_super(parsed, method.meta.name) + dbl_src += _method_maker.get_function_text(method) + + var base_script = _get_base_script_text(parsed, override_path, partial, included_methods) + dbl_src = base_script + "\n\n" + dbl_src + + if(print_source): + var to_print :String = GutUtils.add_line_numbers(dbl_src) + to_print = to_print.rstrip("\n") + _lgr.log(str(to_print)) + + var DblClass = _create_script_no_warnings(dbl_src) + if(_stubber != null): + _stub_method_default_values(parsed) + + if(print_source): + _lgr.log(str(" path | ", DblClass.resource_path, "\n")) + + return DblClass + + +func _create_singleton_double(singleton, is_partial): + var parsed = _singleton_parser.parse(singleton) + var dbl_src = _get_singleton_text(parsed, parsed.methods_by_name.keys(), is_partial) + + for key in parsed.methods_by_name: + if(!_ignored_methods.has(singleton, key)): + dbl_src += _method_maker.get_function_text(parsed.methods_by_name[key], singleton) + "\n" + + if(print_source): + var to_print :String = GutUtils.add_line_numbers(dbl_src) + to_print = to_print.rstrip("\n") + _lgr.log(str(to_print)) + + var DblClass = GutUtils.create_script_from_source(dbl_src) + if(_stubber != null): + for key in parsed.methods_by_name: + var meta = parsed.methods_by_name[key].meta + if(meta != {} and !meta.flags & METHOD_FLAG_VARARG): + _stubber.stub_defaults_from_meta(singleton, meta) + + return DblClass + + +func _stub_method_default_values(parsed): + for method in parsed.get_local_methods(): + if(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)): + _stubber.stub_defaults_from_meta(parsed.resource, method.meta) + + +func _double_scene_and_script(scene, strategy, partial): + var dbl_bundle = scene._bundled.duplicate(true) + var script_obj = GutUtils.get_scene_script_object(scene) + # I'm not sure if the script object for the root node of a packed scene is + # always the first entry in "variants" so this tries to find it. + var script_index = dbl_bundle["variants"].find(script_obj) + var script_dbl = null + + if(script_obj != null): + if(partial): + script_dbl = _partial_double(script_obj, strategy, scene.get_path()) + else: + script_dbl = _double(script_obj, strategy, scene.get_path()) + + if(script_index != -1): + dbl_bundle["variants"][script_index] = script_dbl + + var doubled_scene = PackedScene.new() + doubled_scene._set_bundled_scene(dbl_bundle) + + return doubled_scene + + +func _get_inst_id_ref_str(inst): + var ref_str = 'null' + if(inst): + ref_str = str('instance_from_id(', inst.get_instance_id(),')') + return ref_str + + +func _parse_script(obj): + var parsed = null + + if(GutUtils.is_inner_class(obj)): + if(inner_class_registry.has(obj)): + parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj) + else: + _lgr.error('Doubling Inner Classes requires you register them first. Call register_inner_classes passing the script that contains the inner class.') + else: + parsed = _script_collector.parse(obj) + + return parsed + + +# Override path is used with scenes. +func _double(obj, strategy, override_path=null): + var parsed = _parse_script(obj) + if(parsed != null): + return _create_double(parsed, strategy, override_path, false) + + +func _partial_double(obj, strategy, override_path=null): + var parsed = _parse_script(obj) + if(parsed != null): + return _create_double(parsed, strategy, override_path, true) + + +# ------------------------- +# Public +# ------------------------- + +# double a script/object +func double(obj, strategy=_strategy): + return _double(obj, strategy) + + +func partial_double(obj, strategy=_strategy): + return _partial_double(obj, strategy) + + +# double a scene +func double_scene(scene, strategy=_strategy): + return _double_scene_and_script(scene, strategy, false) + + +func partial_double_scene(scene, strategy=_strategy): + return _double_scene_and_script(scene, strategy, true) + + +func double_gdnative(which): + return _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE) + + +func partial_double_gdnative(which): + return _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE) + + +func double_inner(parent, inner, strategy=_strategy): + var parsed = _script_collector.parse(parent, inner) + return _create_double(parsed, strategy, null, false) + + +func partial_double_inner(parent, inner, strategy=_strategy): + var parsed = _script_collector.parse(parent, inner) + return _create_double(parsed, strategy, null, true) + + +func double_singleton(obj): + return _create_singleton_double(obj, false) + + +func partial_double_singleton(obj): + return _create_singleton_double(obj, true) + + +func add_ignored_method(obj, method_name): + _ignored_methods.add(obj, method_name) + + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## \ No newline at end of file diff --git a/addons/gut/doubler.gd.uid b/addons/gut/doubler.gd.uid new file mode 100644 index 0000000..abcc296 --- /dev/null +++ b/addons/gut/doubler.gd.uid @@ -0,0 +1 @@ +uid://cpy013l0wqwmg diff --git a/addons/gut/dynamic_gdscript.gd b/addons/gut/dynamic_gdscript.gd new file mode 100644 index 0000000..2ce7667 --- /dev/null +++ b/addons/gut/dynamic_gdscript.gd @@ -0,0 +1,33 @@ +@tool +var default_script_name_no_extension = 'gut_dynamic_script' +var default_script_resource_path = 'res://addons/gut/not_a_real_file/' +var default_script_extension = "gd" + +var _created_script_count = 0 + + +# Creates a loaded script from the passed in source. This loaded script is +# returned unless there is an error. When an error occcurs the error number +# is returned instead. +func create_script_from_source(source, override_path=null): + _created_script_count += 1 + var r_path = str(default_script_resource_path, + default_script_name_no_extension, '_', _created_script_count, ".", + default_script_extension) + + if(override_path != null): + r_path = override_path + + var DynamicScript = GDScript.new() + DynamicScript.source_code = source.dedent() + # The resource_path must be unique or Godot thinks it is trying + # to load something it has already loaded and generates an error like + # ERROR: Another resource is loaded from path 'workaround for godot + # issue #65263' (possible cyclic resource inclusion). + DynamicScript.resource_path = r_path + var result = DynamicScript.reload() + if(result != OK): + DynamicScript = result + + return DynamicScript + diff --git a/addons/gut/dynamic_gdscript.gd.uid b/addons/gut/dynamic_gdscript.gd.uid new file mode 100644 index 0000000..3c64ffa --- /dev/null +++ b/addons/gut/dynamic_gdscript.gd.uid @@ -0,0 +1 @@ +uid://cnbjsrik0p5uf diff --git a/addons/gut/editor_caret_context_notifier.gd b/addons/gut/editor_caret_context_notifier.gd new file mode 100644 index 0000000..a1b2fd9 --- /dev/null +++ b/addons/gut/editor_caret_context_notifier.gd @@ -0,0 +1,212 @@ +@tool +extends Node +# ############################################################################## +# +# Watches script editors and emits a signal whenever the method, inner class, +# or script changes based on cursor position and other stuff. +# +# Basically, whenever this thing's signal is emitted, then the RunAtCursor +# buttons should be updated to match the data passed to the signal. +# ############################################################################## +# In the editor, whenever a script is opened you get these new things that +# hang off of EditorInterface.get_script_editor() +# * ScriptEditorBase +# * CodeEdit +# ############################################################################## + + +var _last_info : Dictionary = {} +var _last_line = -1 +# This is the control that holds all the individual editors. +var _current_script_editor : ScriptEditor = null +# Reference to the GDScript for the last script we were notified about. +var _current_script = null +var _current_script_is_test_script = false +var _current_editor_base : ScriptEditorBase = null +var _current_editor : CodeEdit = null +# Quick lookup of editors based on the current script. +var _editors_for_scripts : Dictionary= {} + + +# In order to keep the data that comes back from the emitted signal way more +# usable, we have to know what GUT looks for for an inner-test-class prefix. +# If we didn't do this, then this thing would have to return all the inner +# classes and then we'd have to determine if we were in an inner-test-class +# outside of here by traversing all the classes returned. It makes this thing +# less generic and know too much, but this is probably already too generic as +# it is. +var inner_class_prefix = "Test" +var method_prefix = "test_" +var script_prefix = "test_" +var script_suffix = ".gd" + + +# Based on cursor and open editors, this will be emitted. You do what you +# want with it. +signal it_changed(change_data) + + +func _ready(): + # This will not change, and should not change, over the course of a session. + _current_script_editor = EditorInterface.get_script_editor() + _current_script_editor.editor_script_changed.connect(_on_editor_script_changed) + _current_script_editor.script_close.connect(_on_editor_script_close) + + +func _handle_caret_location(which): + var current_line = which.get_caret_line(0) + 1 + if(_last_line != current_line): + _last_line = current_line + + if(_current_script_is_test_script): + var new_info = _make_info(which, _current_script, _current_script_is_test_script) + if(_last_info != new_info): + _last_info = new_info + it_changed.emit(_last_info.duplicate()) + + +func _get_func_name_from_line(text): + text = text.strip_edges() + var left = text.split("(")[0] + var func_name = left.split(" ")[1] + return func_name + + +func _get_class_name_from_line(text): + text = text.strip_edges() + var right = text.split(" ")[1] + var the_name = right.rstrip(":") + return the_name + + +func _make_info(editor, script, test_script_flag): + if(editor == null): + return + + var info = { + script = script, + inner_class = null, + method = null, + is_test_script = test_script_flag + } + + var start_line = editor.get_caret_line() + var line = start_line + var done_func = false + var done_inner = false + while(line > 0 and (!done_func or !done_inner)): + if(editor.can_fold_line(line)): + var text = editor.get_line(line) + var strip_text = text.strip_edges(true, false) # only left + + if(!done_func and strip_text.begins_with("func ")): + info.method = _get_func_name_from_line(text) + done_func = true + # If the func line is left justified then there won't be any + # inner classes above it. + if(editor.get_indent_level(line) == 0): + done_inner = true + + if(!done_inner and strip_text.begins_with("class")): + var inner_name = _get_class_name_from_line(text) + # See note about inner_class_prefix, this knows too much, but + # if it was to know less it would insanely more difficult + # everywhere. + if(inner_name.begins_with(inner_class_prefix)): + info.inner_class = inner_name + done_inner = true + done_func = true + line -= 1 + + # print('parsed lines: ', start_line - line, '(', info.inner_class, ':', info.method, ')') + return info +# ------------- +# Events +# ------------- + +# Fired whenever the script changes. This does not fire for help files. If +# you click a help file and then back to the same file, then this will fire +# for the same script +# +# This does fire for some non-script files such as .cfg, .json and .md files, +# but the passed in value will be null. +# +# This can fire multiple times for the same script when a script is opened. +func _on_editor_script_changed(script): + if(script == null): + return + _last_line = -1 + _current_script = script + _current_editor_base = _current_script_editor.get_current_editor() + if(_current_editor_base.get_base_editor() is CodeEdit): + _current_editor = _current_editor_base.get_base_editor() + if(!_current_editor.caret_changed.is_connected(_on_caret_changed)): + _current_editor.caret_changed.connect(_on_caret_changed.bind(_current_editor)) + else: + _current_editor = null + _editors_for_scripts[script] = _current_editor + _current_script_is_test_script = is_test_script(_current_script) + + _handle_caret_location(_current_editor) + + +func _on_editor_script_close(script): + var script_editor = _editors_for_scripts.get(script, null) + if(script_editor != null): + if(script_editor.caret_changed.is_connected(_on_caret_changed)): + script_editor.caret_changed.disconnect(_on_caret_changed) + _editors_for_scripts.erase(script) + + +func _on_caret_changed(which): + # Sometimes this is fired for editors that are not the current. I could + # make this fire by saving a file in an external editor. I was unable to + # get useful data out when it wasn't the current editor so I'm only doing + # anything when it is the current editor. + if(which == _current_editor): + _handle_caret_location(which) + + +func _could_be_test_script(script): + return script.resource_path.get_file().begins_with(script_prefix) and \ + script.resource_path.get_file().ends_with(script_suffix) + +# ------------- +# Public +# ------------- +var _scripts_that_have_been_warned_about = [] +var _we_have_warned_enough = false +var _max_warnings = 5 +func is_test_script(script): + var base = script.get_base_script() + if(base == null and script.get_script_method_list().size() == 0 and _could_be_test_script(script)): + if(OS.is_stdout_verbose() or (!_scripts_that_have_been_warned_about.has(script.resource_path) and !_we_have_warned_enough)): + _scripts_that_have_been_warned_about.append(script.resource_path) + push_warning(str('[GUT] Treating ', script.resource_path, " as test script: ", + "GUT was not able to retrieve information about this script. If this is ", + "a new script you can ignore this warning. Otherwise, this may ", + "have to do with having VSCode open. Restarting Godot sometimes helps. See ", + "https://github.com/bitwes/Gut/issues/754")) + if(!OS.is_stdout_verbose() and _scripts_that_have_been_warned_about.size() >= _max_warnings): + print("[GUT] Disabling warning.") + _we_have_warned_enough = true + + # We can't know if this is a test script. It's more usable if we + # assume this is a test script. + return true + else: + while(base and base.resource_path != 'res://addons/gut/test.gd'): + base = base.get_base_script() + return base != null + + +func get_info(): + return _last_info.duplicate() + + +func log_values(): + print("---------------------------------------------------------------") + print("script ", _current_script) + print("script_editor ", _current_script_editor) + print("editor_base ", _current_editor_base) + print("editor ", _current_editor) diff --git a/addons/gut/editor_caret_context_notifier.gd.uid b/addons/gut/editor_caret_context_notifier.gd.uid new file mode 100644 index 0000000..22cec65 --- /dev/null +++ b/addons/gut/editor_caret_context_notifier.gd.uid @@ -0,0 +1 @@ +uid://c110s7a32x4su diff --git a/addons/gut/error_tracker.gd b/addons/gut/error_tracker.gd new file mode 100644 index 0000000..c53c957 --- /dev/null +++ b/addons/gut/error_tracker.gd @@ -0,0 +1,206 @@ +extends Logger +class_name GutErrorTracker + +# ------------------------------------------------------------------------------ +# Static methods wrap around add/remove logger to make disabling the logger +# easier and to help avoid misusing add/remove in tests. If GUT needs to +# add/remove a logger then this is how it should do it. +# ------------------------------------------------------------------------------ +static var registered_loggers := {} +static var register_loggers = true + +static func register_logger(which): + if(register_loggers and !registered_loggers.has(which)): + OS.add_logger(which) + registered_loggers[which] = get_stack() + + +static func deregister_logger(which): + if(registered_loggers.has(which)): + OS.remove_logger(which) + registered_loggers.erase(which) + + + + +# ------------------------------------------------------------------------------ +# GutErrorTracker +# ------------------------------------------------------------------------------ +var _current_test_id = GutUtils.NO_TEST +var _mutex = Mutex.new() + +var errors = GutUtils.OneToMany.new() + +var treat_gut_errors_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE +var treat_engine_errors_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE +var treat_push_error_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE +var disabled = false + + +# ---------------- +#region Private +# ---------------- + +func _get_stack_data(current_test_name): + var test_entry = {} + var stackTrace = get_stack() + + if(stackTrace!=null): + var index = 0 + while(index < stackTrace.size() and test_entry == {}): + var line = stackTrace[index] + var function = line.get("function") + if function == current_test_name: + test_entry = stackTrace[index] + else: + index += 1 + + for i in range(index): + stackTrace.remove_at(0) + + return { + "test_entry": test_entry, + "full_stack": stackTrace + } + + +func _is_error_failable(error : GutTrackedError): + var is_it = false + if(error.handled == false): + if(error.is_gut_error()): + is_it = treat_gut_errors_as == GutUtils.TREAT_AS.FAILURE + elif(error.is_push_error()): + is_it = treat_push_error_as == GutUtils.TREAT_AS.FAILURE + elif(error.is_engine_error()): + is_it = treat_engine_errors_as == GutUtils.TREAT_AS.FAILURE + return is_it + + +# Marks any errors that GUT should not fail a test for based on internal, +# non-alterable rules. This is idealogically differnet than _is_error_failable. +func _auto_handle_error(err : GutTrackedError): + # Issue 842, when the source has an await and a declared return type this + # error can occur when doubling. It does not appear to affect anything. + if(err.file == 'modules/gdscript/gdscript_byte_codegen.cpp' and err.function == 'write_return'): + err.handled = true + print("[GUT] \"Unresolved return\" has been ignored. See https://github.com/bitwes/Gut/issues/842 for details.") + + +# ---------------- +#endregion +#region Godot's Logger Overrides +# ---------------- + +# Godot's Logger virtual method for errors +func _log_error(function: String, file: String, line: int, + code: String, rationale: String, editor_notify: bool, + error_type: int, script_backtraces: Array[ScriptBacktrace]) -> void: + add_error(function, file, line, + code, rationale, editor_notify, + error_type, script_backtraces) + +# Godot's Logger virtual method for any output? +# func _log_message(message: String, error: bool) -> void: +# pass + +# ---------------- +#endregion +#region Public +# ---------------- + +func start_test(test_id): + _current_test_id = test_id + + +func end_test(): + _current_test_id = GutUtils.NO_TEST + + +func did_test_error(test_id=_current_test_id): + return errors.size(test_id) > 0 + + +func get_current_test_errors(): + return errors.items.get(_current_test_id, []) + + +# This should look through all the errors for a test and see if a failure +# should happen based off of flags. +func should_test_fail_from_errors(test_id = _current_test_id): + var to_return = false + if(errors.items.has(test_id)): + var errs = errors.items[test_id] + var index = 0 + while(index < errs.size() and !to_return): + var error = errs[index] + to_return = _is_error_failable(error) + index += 1 + return to_return + + +func get_errors_for_test(test_id=_current_test_id): + var to_return = [] + if(errors.items.has(test_id)): + to_return = errors.items[test_id].duplicate() + + return to_return + + +# Returns emtpy string or text for errors that occurred during the test that +# should cause failure based on this class' flags. +func get_fail_text_for_errors(test_id=_current_test_id) -> String: + var error_texts = [] + + if(errors.items.has(test_id)): + for error in errors.items[test_id]: + if(_is_error_failable(error)): + error_texts.append(str('<', error.get_error_type_name(), '>', error.code)) + + var to_return = "" + for i in error_texts.size(): + if(to_return != ""): + to_return += "\n" + to_return += str("[", i + 1, "] ", error_texts[i]) + + return to_return + + +func add_gut_error(text) -> GutTrackedError: + if(_current_test_id != GutUtils.NO_TEST): + var data = _get_stack_data(_current_test_id) + if(data.test_entry != {}): + return add_error(_current_test_id, data.test_entry.source, data.test_entry.line, + text, '', false, + GutUtils.GUT_ERROR_TYPE, data.full_stack) + + return add_error(_current_test_id, "unknown", -1, + text, '', false, + GutUtils.GUT_ERROR_TYPE, get_stack()) + + +func add_error(function: String, file: String, line: int, + code: String, rationale: String, editor_notify: bool, + error_type: int, script_backtraces: Array) -> GutTrackedError: + if(disabled): + return + + _mutex.lock() + + var err := GutTrackedError.new() + err.backtrace = script_backtraces + err.code = code + err.rationale = rationale + err.error_type = error_type + err.editor_notify = editor_notify + err.file = file + err.function = function + err.line = line + + errors.add(_current_test_id, err) + _auto_handle_error(err) + + _mutex.unlock() + + return err + + diff --git a/addons/gut/error_tracker.gd.uid b/addons/gut/error_tracker.gd.uid new file mode 100644 index 0000000..40ca392 --- /dev/null +++ b/addons/gut/error_tracker.gd.uid @@ -0,0 +1 @@ +uid://35kxgqotjmlu diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf b/addons/gut/fonts/AnonymousPro-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1d4bf2b5265bfc9dcb92eeabd5d3691035361a9a GIT binary patch literal 107624 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 literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf.import b/addons/gut/fonts/AnonymousPro-Bold.ttf.import new file mode 100644 index 0000000..23867ed --- /dev/null +++ b/addons/gut/fonts/AnonymousPro-Bold.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://c8axnpxc0nrk4" +path="res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/AnonymousPro-Bold.ttf" +dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..12863cadbec64a3f918932540eacf8d9c4d86c3a GIT binary patch literal 95872 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¥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 VM@#U?4Ko8L0}}%;1L!nJCIG@;7E1sC literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import new file mode 100644 index 0000000..0ec1e76 --- /dev/null +++ b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://msst1l2s2s" +path="res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/AnonymousPro-BoldItalic.ttf" +dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf b/addons/gut/fonts/AnonymousPro-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f6870b7c4e5af796982cc026e159e844c5ad8233 GIT binary patch literal 98424 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 literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf.import b/addons/gut/fonts/AnonymousPro-Italic.ttf.import new file mode 100644 index 0000000..b5610ec --- /dev/null +++ b/addons/gut/fonts/AnonymousPro-Italic.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://hf5rdg67jcwc" +path="res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/AnonymousPro-Italic.ttf" +dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/AnonymousPro-Regular.ttf b/addons/gut/fonts/AnonymousPro-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..57aa893826db03a74c98480093e5c9942798391b GIT binary patch literal 112072 zcmZQzWME(rU}RumVPNnN);ID>+SkIs805gfz>pB;;v12*w8(~mF=zn;1M7q2+{A)O zLgJGc7?UCx7?>X=mz5~6baBmNV9fAgU|`ToD@f0sV?+Z31Luv5)Wnqk+Ov)@F#0ku zFff>9WTYl?r8AvlU<{IBU|>+m$StWb$uhmb!02niz`*h-CqFrHBWu!f21XAV5I;Au zqJX7?Igx?U{SN~JgF;?nZYoQ9Oa=pEMhydlbyh)samlk)X4e@Q7r$U&5Ik2k_^lY>Th7Uvw=fk1LK7ajQSf{c)o04 z5ZK7d@r7XngKNYFCVlCROrmy?U=D-6^hO4LyU2|!Y+p7o32bCz`(j|Is3@q&s3@q& zT>NJrv(sO9CjGw`StWnRu;?=|FflMNFfuP;UdkZNAkVOOgM$7BMg0v-cQ!Kfe%YWP zuz~%;26p|8Y`kAKunTP9z5pWmFKl2F*dTae1GB&e;R_p>1U86*NlCDX3|Lm~0>p6w z`qCQ(grSZT(U;yR0^@+4%@37f)0f`J#%~w7S(=f7MSC+pBM*!A1_pr*EEhl|$At~j z0vot4Y~UBzAOt4GE^Oct*dPHGmjSb7FBllADvK(cDvBzbDw`S$Dw`Uc8jC8L8jBi> zDhnDb3MvXRFS%R#pP}^b-Me>78Nu|wb^jUef%tih9Jx=PJb9A)?*}8tzaI0(y|6)6AEa3U zY=r&=2Wd8AF=I7jGh;X7Fyl1iGUGPmX~xTpw;3NZ=8fHQ)%d9~Q!FS@%)qwU-eK4v zs=q-?f0MqJsGyMLCNnT)V92V+tZptQCN8MQY-(c1WNvCKD#t9&uBN6gC?+Nk^#wa2tF388s*c;NB;=t1=-dGaS9HVa-)g030T_0n?*3X-fux*=RTzgm- zV`6SrN#fr##YyHt#*zYD_VHCNim6jYnfT=$LvlSG3ZgB!mMr0NNh-IMjF=?yPt$q* zWS5F~M~+oYOktJ!|C9u4ncn)Qt23_R(6)49U|{|K=KoXXgRJHZ!VKaJ+6?9l&Wx^` zjkvg2v^O(wfKT8pAS%Z4Ad4K!L*Wlz}OB zladaDppfMTCH*b8lwK);LtjZ!NmI#E$x|s(DO0IZsZ(jD(n_VBN+*?gV;%U@jLMAK zjHVeaGumc!%!obKftSrl%t+11%*f3s%!n;^%T=SNMoh6=Zdtst0GnnZX`yLhY2j%R zX_0ABY0+sh(_*E?PK%Qkys;bYIKOO=5ZK^)hhYQ1{swV$# zXTXbY%SEAkFS5?x^l)?&$6q?wIaa?%3`)-Eq0&cE{t6ys=xZIX-h_iZw7~ z6yal1R?=evMZU3-9Fr=eI-@X}7^8`rvJ#u9h?udF84CNMx@(Myag2+anoEqaNsOyH z&o6)O3k4 zHjW3WjGyxBtqrT#?{%h8ZtChTF{UOl&g$yUG0YMU5fKjd5fOhrLo|X4^#9NQKV{y- zs?VU#V8r0W@OZNU2Oo>}W;qUPa4IkmGf*=yGjKBqGe|QiGiWoIX0Xg)o53*yP%1EZ zYQPk`NsP|`lnTW3HyU$%*>cPJl{F|}IjtqFHLWeJJ*^|HGp#GFJFRD0ue9E2ebSmY z7E)44>Pv5wgcYN#`qCR&VG+lvFTIfyRHkaMgF;_JTti*MT*F->Tq9khT%%oMy2f&i z?Hb25K%uYkOoJ(QgQUPlJC-k7ZrZ)H16j^tCt;^yXJO}I7h#uSS7Fy-H^Xj)-444G zcD$gNVq;?$XIE!qgGHCQnwqJJ87R`g`IOPj%$(84%$!l34HQ?*B4Xlp%%-5I<6~mp zBNSjgH*T)imQ0`a6dP%M2URlRmhv5@y)I2ubwdDoE5W(!h|BS(%wh zUpq~cRq~%U<5dPmhCTm!nGUkvXAotu+bqD#3XW3&F#$CJGXXb&Fo86IGJ!UMX#&dx zwh0^)0L7`mQvs$}h7DXuMhk+C)?;EaH4!p1GZ*J$78Q|W5@)K+URGdZQ?M-CBYtYS zscHJ;#DBdZS3faww6`;|f4mwJ<#XWnyUOafHxK(TFfu&-*UR(<>JkQCZWiq=x42(% zgJO%5TasIo+mhRpJCZw-yOO(;dnWfv?w#Bxxj{)q5aJSW0)e@H`@dhGK#n+g`)y_Ahua5yqC&2I{P&%KfssL( zfswh9c`1V+gF7TG^65)&Z+>dtjxyDs?3f5RxyT||J%Y>!uM~BIb+zrRpyL7d?kF0K1@A3_Qsw5SIgLZ zI&SZdgtLro|EkV{+{M7a%e;hn0|PgM`DP|wW)^Kwjn59Q>p3qlY+!-}1&6-$Mh;=S z$j#hr&`Ocz!Uk@E4Qv+-3>g&#l?4?|6$On26`7a(%P9Gm!8oIYaR%d(f0>NaOBtvC z%Vc0=`0%frDT!eh12cm#!v+S3sf_y48yUd?tjee?%#`%+UBa$ie=joWgIbG>OPH51 zfm%qM44aujwt{RjRb*cBCx&?m%kE@TM6^3CL1$+wg5Bp+|=CPA=zNCFYpm)q5(tx`IH>3o z2RHk+++=&n1~Qa`O@d8>&4SH?ErKnBt%9wCZ3f#4wjFFI*mz?%C<<)kW&@RK+~VBo z+~(Zw+~M5m+~wTu+|#+2b8qK9&J8NnxSw$|#cmP=8*gAJEF#XRrlzdKBrIlZBqV5} zrp{<$#{?^{K&4oQGE=faZ`_96LCM?J>=P3BbHt)-e|!7>3Ny2^{hgir%T1ZOC)P8y zMkV~a`nPriqc$UpN2B$>Kh0nE6c+CJ(%k%IS7G6 zW!1ab_LTW`T`Fa+){ONv>GLU@=Jh*eoNfzzRoE$W)9K}%qxA}G)lUn+uJ!J#OjsG(?~=%E;)n4wsq*r7N>afRXz#S@AkS4c<-3RxnW zJ)pL)F(|f8^q3&g4yrKOM3vzwm5p7FnR$j=>CBM$#kGDMtvtKBtTX&{jT3rfUFuWp zCY4Ou(CAdQEHz|eu5D|zTZWq|V@6nSiiJtaj3lAyGt@m2tOHUlMM}j@BV6)YXD&%w zly7Yq(;AjPS(wW@IuFvbVqjjvD#^gdAjlB7nU@EYdU*vlaDe+sur$oBFTIf+7Fw`0 zyqTYw1>C^^C1y~!hx5V)7I6QINniu-g$;bLM6GDbXevn zU%vcr$v@9hMuu``-9MMh|1&U_Gj;zh1*HI%|1X(Wu}U&%F)Z7lt-nDB)Vtg$%JXH* zEzwut)X6C-DXJ-IDe5U2DVix-DcUJIQ*@>1PSKO1ys;a!1vbcC*dV9BQI_Y+2Biy| zl;uFt4@v*>`qCTaVP1hpyc(MnB;p0Y5icMvpe|r8;4Tm@kSH|@HkasV^BZ5?W)f25 z5>OkVuOC+H6ENAVliBcZgw4!=)D=}OjN1}c)OuKC%t#V-DGky$jjwh0s*f>sc3|o$ z{g+{7?^wQufr07&kN+>3_p$0TC^48Z+}|w84epQdD1+N8l46o-l4g=_l3|i*l4X)@ zlG7xYNp6!oCJAb|+M2B!d8h_Xa`z4MO@GSnq6P%)!kO<{8yf0%VvbFeyjODET|7lsTPc zLF>PN|DOLlcyJq|6Qe34SM7fW#!?1ma5`8A?u{5TxHB4Vw&MY%1UViJPf|?MU__;ti%0ZgVPRvft&dkouF3c{?uFS5@ZkpXP zyKQ#I>_7?4?x`J9>?RQ|JCN}r`kR!&x;E1{yF$A|dxrK3?H$@Dw7FxqaA-?tGsS{RaA$DZau#=1 zcQ$u+cMf+>cP@8scb@LN+;(BN3ov1@70#F*8HTWN`me7(Jz} z^J-j|l(MGLOWiZm-aO1&(XDJwNXVQrH%03R3x^CZ%^h>v+viN_@1LS?9$(?&QW0+s zqZ#wlCno6WB}_~=&55<;V6zE`(~fA3(AAA-jnIw@uwmn{jm=@w%&V`@ORuW>$LXGG zFDq-G>h6~2qNwPS#=!Ld<^Pw=8(8%jlo`w!o^DnFjrMHj;?QH!2IX2QZcsZB&>4CuYm14p@@q@JdprJko=q+X_8rCz7r zOudzQJM~WLfs26+me7h5obh4hCpbxih6^;=H&mz(!+G#2UXe2DyU6Si)Gt*uvPuIKnu?xWc%@ zc!u!`;~mB)j6sbKW_ZO28aIGfpM1=q_MAC1`??i=BrWqjbw% zGw0`;3izZ|TBWV2cI$!_1De5*Vt_d&cTJ^Z;hs;;tzY)!cLW$22K1k-G>&pocdK5L z=G7QyY8X-LN}{iGct-ZGK={BV_;(N{THC?&&0sQ z5b!SmJl1-gc?r`uu)p{jHnX#_uxM{)W@cc~hV)=SeI-aQ2Go;bsxSY05nN~Y|G&p{ z7s(!0eMldL(VXe-KL$U>4gUiC81MaK@Mqk}xFG=K+Z7B9%)PAo49pCQ3==kUbMUZe zZ)V{TX3+)>oPr0EH}D8-WCnFUgkK4R(g&xoq_C#2rLd=Pq;RHirEsV4OyQNnJB3dQ z^TvW2AIi{Fic-tN8Z4kXTMU%g#9oSl4C4@!5YrH|5c3d=5X%s&5bF?|A+|znhu8@* zP+|jhzZgaMSU}A&7U)n0yE>>Q7X!r_W1MAJTA0;D>#)o)>zs7^ct>@%YN5j$mhIZb z?BZ9N6c&@~>Q|K=7n5umk)r9EU?sfb&?*Kdc>m0wA%W3hvk|8Yi}vOS9%0b<5f3+b zL{W`J8`L$GzOZej+D^5TYRs`)ZmGReV~Ta)^;C;g%T%jW>r|Vm#un?q$*Cr(rm1GB z#uDow7-obzR>%?Sz|969D`bU@6oQNnxZohj79kd)7GW0Q77-Sa7Eu-fQr;FZEn->3 zwuoa9AmtHHBS6C%py7LOed&$fuuN~OFTK$gll8- z4weAr?P!4wiWfj5_1*#-b(y|wxvBe77aZ=o61p0?7P=m~5xN<=6}la|Gjvzz?$AA< z%Nx7FR$!w!6R5LiE^e-FZf@>w9&Vm)UT)rQKHYq|`F8W;=Abgf{FymQ(QS$tnKCvK z6E`&$1QpnJOr|D!Ea0XBxNtBt7Y7Ysv6!0Jv4DzrRZtI+j|n`uXvbvAd|b>d%30P- zOHwS(&MCrFOvxqHS=mpbSwdV!LxNjS-7d1!t8hlFhDPknf}kc->xLsS6Z8Fz#3hXU z3noN^x{m)E#qHJ9%vA&}O;l~P#F*B&^b~rCs2MBBc*j{KR5^IHr`sy4n*RN)AY&Gs zqVL*So$0Hcu;@x@@#RGcR)#K(#xZr%R)j~coK_cYoWJcEi=n!%cf4V4o~~!KE(6p5 zqYMnpk69%d*crGO3?L-~cv=D$H}IqmiZxhqvzddL36yyim6;Wpl^KnNnVFd%Ppt>bwbCqe~-+0ih0%&$;5x7qv&7i{2zFCn6R0MGI@Pkw4Mj5Uz z;6?y13#bvW@=Nk-@>}wI@<;M#@>lYA^3UX7$-k5TBtN(h51N4C7TBN$ z&7t5j4CZWRed&$Npd=;ED9@t3iB}#xM!+htQH1Tw24-+e8`N!LQ&tpIG?HTy6=4@+ z1{L!lt{s!Gi5ghKjCs+g`~MOcJA>xc`knr<%_=I#=syD^gGEl5h3@*9|6VeR&s@(~ zx_cL^q_kIa)@d&v#zfZ$eZhaOf`(y#zqD8X{nyCA`2WrSm&|Ke^%+zd9z#b@gziAQ z(`w+35r_T;A%Ts8yk9nmT-d-Nuu%>?rXnXMrzU46=Oz~>mnK&x*CsbjZkgORxnpvm zx>oL~98)Z)fv9?6gQ`9x;so`jHwwZ+0Nhf9abUr*Nr0Ibl#zG^HcEoZEy@^a@7X;igK zR(FW9a!7a9mjuSJ6-PmBXVADvSj3EfA+FU?#wm-AB_|!9pUS8XD(6Az={&0>0}q2J zL%{|y$kfA~4Pv12w+(Fi8yEyO@`Czyysvn{35i#dSCiM0*ONDrHhq~Y!rgEA5prc-~^Pu3ZR4{$HXqc1WqVI%;(K3 z_qYFxU|i9 zw3Ah`^q+3&KS^ZUI2p_~v+=OAXoF@l8KLb7@IWTaHgMJlWgBKt9Sur~3?OSj)2O15 zUPSR~J)pXTz)pphCs>@Zks~%ScrD)Y>s^An2ig5U>61Xxz zP10uOil&Op*Fi}DXS$xZP4M4+#!XC}t)F(~qa<(l>Iiu9{&E-G@7&Mk68V6vaZA9pIrGHb^Sv zxUhi}JZ{c)VFNd8>;^nn4IPZU%%p$mQYnjmY3c8a42=K({eKBw10cxYvzY-j?+md= z05qEp^9!sb1EqflX?8AgE_E(*E_beQu5_+)u6C~JT+6w(a~-@Z+EQL|TZzX!8AEYHkim1N*%@Y>A61DdpC z0WAxFxCq7P;E6Jr^I*Y%64aYG7}&tAYDHy6c0px9V?jn^M&@}d{`qx*0(jY(f@OUmP74b0S zf|3v)JQeYRyaHa~!1HAj7dxoZg;;=67;x)LZ{)`6F|hx^WA6+cpynAXgyI5I28N2t zg3QXyAddN<$L0*^^?ib#rRidc$x zibRTJid2epip&&QDY8@KqzJf!3$De**|65)+_77>$sLnpirsP*T#-XUA8Q#08qY!0 z)eh2-${Jfm4Jp=0sHjnjH901XVXxz`c?;7@&^$(&MVDbrdsudNh@xUhcXn1!hyqhL zbkd?3R9S;2F&fWL@`csasq>FRSN4F~eq0Q~3`HA6^g$~(818@ydbSH2*z`BDLSmU4 zR9_P@8M%Q~U<1}sWN_*L)m?&Ypdn{=@R&QSk*6%EXe7ptXx6>C`tN2Z1m zyUD2Dw~Bem-W{L?Lc4dhSN>zD1dWH@|JMti`BGpoV3gTlsK3DolpZ!Jg3^HEEAa3< zr=p~yrlO^yr(&dHredXHr{YY-m5MtRPb%`pZs528TF9gba_UP=7FBx*>L5yUs7a`4s9C6as70t{s8y(SsLfDYp|(Tqgc@%wsL`s& z_yrWkd`#?Opam7?qM+8FnmS_C3!bV3gp7@Zn2+mct|%ApZdW!Gmo=3w|fsF#77!-IV0BR<13P=iQ3Rntw3PcKI z3RDVo3d|H(DX>%EqyS_9Km^)O0+)HP!Uv_if!C;FjG+DuzrY5j3!rIYHi3=8pw^S3 zGNUqRLf8nh0twN%*s$(j^J#D@v`Xm?db@POzuSze6P7aRL(*YTbBqNWV}5HjC^<4R zNdAAx+{~)aV9#)8GY^*ni}q$MZcP^L4VnVr`70i9PeM~*gU*Ew3IZF=c)o1V64>Af zZA)qDOK;SKxn5gedZRXM2uW36dZQ{#23(h+C!)>PEYd96n>biiL0tz{fsKOT6eTDw zs4i$O=q?y8m@Zf@*e*C-aJk@i!Q+CU6eajfkSTVP21JXhz(zw*k}`a02nulyLkU9- zLkmL>!wAC+!wSO=!x@Gv40jlwFa(W~K$h^Rse_iUf&19tJ~tCQMPW3*#O0Won_Y^g z1mx{7D=D!{sCBeT^3pbsEwOR$D{ynko8%k6P`9$uFeJyuG{HmLIIPgxYf_09V@ObI zf=N(7H)F4Jet@o`ZG?q=l!LCAqtkhZnSb zaf2W1XEV1W)!S)f4Y6TsJcVpIk-(LihazzsG<<|U>7UR?Tn@$$bHrOZp16Bu27 zUo0(U(f_xHaVN-?hS1iOBtroxGf0EVt_@Pqx(w7k0`-!_VRb7?WiO#Gy-@<@JXl$> zQ3xFELgGT|Lgqs5Lg7N`Lghm3Leqto3vCxVE(D5pp=Uyn78htZRgcM3&{z-_hoD;5 zFnwN5?%Y(vPEp(3h=_b!kuJuI`CIDU-0HXGGrRrSVcnAB;gQo~&FsOz2p^eXV=#nN zkKjxPa}~IfhjGBmCP3{GRz^@;1~f{j%xKKW$o%-9&#r#~6B%n6t63#~J!h5tUBuGI z!1(_lbku`~Argz(;0S@44bBp<%mT9rwRy)3wsjK+gyIHMkaa-J%FM>X!p6eF%#U9$ zYHeTea^jBW_LdzWw=mWIZDy7HeU54GUuPC02FCv{85o#%u}U&XGgLv`gpx?Wi|Sx* z0*3@B#em9saGw=4<<0~e+PJWZg#ol+W&`Vm4Q$}{H`j#?d;%MJ*uHEKxUfO+!X_bz zyzm9kV1xJt(0WoXfelg@Hb_I8=*-OE6{L)e;HJ8;FqmRwG-lrQuP@^4`G|kL^^DU0 zUga{D=AJ&9$yi$W?*pSmeJSIbe<7u%OfpQurKSJk7#Ee6{(bxR2?MBi$iTq7gH@72 zgdu%1GY1EY_9g)iWTsLUt`_JkmkCzy9+{W}THFekGZ3vwPm%wa5GS@O64pHpcm<36Us z($aq-jBnumuX0vN2405H4Sf2b%A4U1Xhag+V%flWVG}bOD4~JI4p_lCdm}e5bP5U_ z9Wd{}BZ^&MBgpB9e!D4Tk&q~8#RGHspIuB7{yr^bHvC)8qzTT%2N<`O{=UfQ$^cr` z^RJufCF^Adb_U(eY@DFc0!D$2418ZUFhlDmur-2qk(-$rp%dtA;A$Bd@_)`?U}R|hFTng1ZT$}`JEJ-C)1<#lNqbD#*o{Fwzkl6K`e2(4HZ$;n zY-SMH$jtc#)bE5%NWd*;hgr@9vD{PIhK$OLjLMA0%*$r{JNobV493zOO!`cze`hnj0+nx| zmC8&0?*^}6f~|*Q1@*(h>!EgmBv-rprvXnI1C*wd72nn!;Lg;Pp$eq6D@u7F?95=u2-@ z0kvIO*$h~;H;97=oVMH)cnNNza0o~UXb4yccnCxYWC&CUbO_84SRt@O;Di8g>?U~? z@X)4;z(#d&`K&IkuC8vb?yerLo~~Z5-mX4feYyH}_2cTG@>%_vI#cWh1Az^?7dGhX z8yKoX=fA`ub6jkY=?FO{Q9)%>&?J}|D`+)|DST9$DWZR8f~vKtnSym#oMOfXAG?r? zwLX(e-MUnwQ&Mzy?qe(u>r60puGaPHOtUNg6H)Fe&&0yU;_MP(ENY>jZtSQY*b?)1 z-W**QZSg)yW1leP2recOT@O?1WbnA&|CivkBO(kc3~x55>Vt+$8SZRQh1FV|JYP2O zT-d}5p7sURU!tIv95E|6I6-UFAq5UfRRj^%NP$NNkXt>_#T}4_GrJ(DU2Vt2yu_(`U5-z; zO=qV~w@=QxYNu|^klLVmr8b>@LDeDJj2E-FGr0pYrTunrOUe8FQ_&|TN{*I%fd`8JQ}xTgF@HT zlrg0A?3+2p6CV%fsLRB6AQFMaRJn3 zVgP4yPH=0Ggk_nE(1w(}A&f z>sDIIw5#;MX*QNp7Pi)j;QaEhl)0FB7lS;59wXxhefGX{2eCX|!of(^#glP2-pbD065$1?@55ya3)9Ca{s8;|pjT5x>9&1!yBk zPG5SX94sWH^rbgS!8qU+3anTM=PVcpHfXY$hXpjmD?Q#rZBRy%64s`(!E4_*lqHlklr5A!lp~Zg zlq-}wlxHZfP~M?@LYX&qldP07xUg3R54=G~q|HoC*_4&kAUjq-om+NL_(JE9&CHpL zoyr#`cOQ07&|Z|$x3p@`?O%?S>vDbit-HFcd;NlHgS3||^Xs+dN|=^mo{{9`7}4LJ zlJo0k*5+nUQ`3-}pxo8zw_u;O6R6Du8xa;}=!1{MflDnBfekztHt^_gkx0~TDZf&x}hU?VFiNLXL8g7X=x1gi$C1*->Z1ZxIs1#1WE z4AvE_J6KP!^2ToBX8_Gdg31;_(6T5#P|HKmSWw7VL6Et)E2q+@I!w2-Gx;B51e1Mj zms{!dpnq|!l7AO6GW=_0V1my~Ff;Hn#Bb)|V`I_Y#K;Ej2{7tIhebBBb9@0!c){jL zPzKRa@)9ijfc6i9N)`?_utuH>8+bvh`aw%#6v2D!6a^vc=>J~4e3>!!5=-3fU(Z2l z{?A1QCI*&&rQo&O%nV`-c90b}oA|+f!VUcT8%1DKjVR6lr$f+g0xs~pn-IAD2WkMD z8Z#;(W-gRX6$M4um|pJM^{@C_Ql?62m`jYQ_+`eJy)1FNf5x2h@Cj1)NONM=FZ~m< ztWU`k*BOqM(7mP4BrFV>dZ)K?Z2vGlDqaQ5v2vpqS?5__9G% z02DbJ#4l`;5aj~RYRGUgf^s;6zy{bNATfREjbgAsLMdWU3LlvB!A*OVHYKkr&(F0f!n?o7D?@( zk@kPx%uB$bN@O2RgxD}f@1wzj4qRo!INPAxgX#B_vVDL|>ZQ|ue-esYt z4w^&+Wi{B085^RH=2E>b#|zX)tDls!uG*zjJFGUyuieI`-7ly%Oq=my?j}$l?VmZT zWLS=S^VTfC&J-&v$A3JKJ{l7P!@q83P&-MOL51NHwEwFL8;MX8*uZ&bBL~Nq4cr$t z@i1^g3MWvZV?A?2ukCB!nHL*(Jp#)g{d(-6g{%(+Yq*3HjwLWKJeOH%tw^z>kDp>ehTf@RPYiqN6PS~&K zj3-UaKrL^-_7p1%N5(JURuHHU%)r1rk9ik^I79Ph4nEL!dk*kQA<()Ce$djZ4T8`R zLKznT7uzTy1PT-paO--@O{SMj;6P!LVA5c+VDey!V9H>sVCrC+!L)*D2h#~A$aFBM zH34o)Y!c!U0T*kGkWB~h2DYN8kfM>8ICG&_T1ZQb;bn01A|{TJ-91y6Sy9!abb7$Q z%izZKD@Fz(aR$c!-~PX3?q=S_V8n2LgR%Yw6Hu#qBOfTg5wW*!gE6SLu|YzAqb9id zttqCdrfH_>rWvN0rdg)hra4V>ndUalW165Or1?}6);9%@T){#EoOnR}F+En$f*)4! zNV9`9JC8VzI*&PzJ5M-II!`%IJI{2Ui@*prk}`WG&K&(xAmJ<@9SQ^Agub3pLxKN1ntZyTad8N?0P>&rl1@LX)l{HN4Ev) zhVnuj+;TQ4L2hX-3YMnX7P|bZ*1GEYU}38iP#wtt>Ysu~>037o^MICQae$l7!l11h zSlY|rRxvEF!1)Qrfenj*Dg;ipFQAhiz@yh2m<2XUfO{bl;u7i-<`V7_;S%W*dEf5^bVe3*F&0~^W=63n6C@P~0=6FZyGXOIs6%h~ZS zYYC$tqYre6`gvwU2FCwSp=P7bAi<0Sw`O4+aE%4yz$^l{4dG2|VrGyI?_DspcftNi z?Grnuwu9WlwB~OJ^OC<}Os_x~I?u9;c?p9wLpda3z+(t7o57<C1V0(Ky~%Mf7R9hHvC%viot)S%mV)qFDxNJT0ct0#aP(#U&12B07ft7CBF-jY?aGI1f6tynfL#>$gB^_ z-TDj+%;n5W7t8q1bJoiYY7B0hl{wiUD>r#SXU%L>&=4_ZdcH%`C(SlCb~)(82vsLt>E*GpwrM_^I~c#M_!nlE=A&7-f^n6y zw}UnthpvN{663lR`PyD-HvfWFFfjiA!|;uH1=9uwWd;UDBReK@anJ+_WHSJ}I(XwC z3sbkMv7#tPJDav!nuBeKrM9M#5?mz3$u`7VTT`F0R6nTLkuPdifs?*lfUCZKP=OPF z#Oy*xeb*p2eTM)4|AW`$v-L2kF(@%GFt9W9FfcF{GcZHNyBWk7BpFm0GB$I8R)K7} zrSJ;eDC1O+RM1qgRPa=YRLE4QROnQgsjyOEr@~24qfA@@lnfy01YCc^#&f_$1Spw; zN@(yf1gJ7+Wm0C*-Ym{609xfQ3~rTy7Rj@*Gb)2u{Hv)kGBXQFWpCD%%?~E`;s`m~U*)xaaomkN|V@01#$f1yZ1F8>H`)Ci`0&RQs?zmd*In2HGXS zya%Ev0920s>jkfu*JF6KS)E4^w1)?DR>fv%UeKOeDbV>r4$^GuV(Mz@X6kP0Vd`n> zW$JC})6|!#Z&N=8n&`T!{uDIPB`vVQ0J`D=+*^lv8r+YEabSrHTq1)j1z2!^XQn~r zA}8yYO_Cg9pa!>?z(yrdqebZ@_^cfcB?%=BB?~1Fr3j@Ar3$4Er5Q>qly)ecP~wdR zZ8g#XA6=s(uA{DFuH&u~u9L1)uG6kFU1zz@cAeuopsG&inGUofXBQP=X9ML-Q$6tU zMbM2WsB;*?%xiQH_6J2wK2^Ud&k*Ml`l)+GBqvA2mtGa+DB1bC9<*)>+gken|G~RY zi`kAdsWGU5Q(HGEwZX-Unba8c!Q$5;;*kAD;M8dV76+$Jka|$-0W5BYA`WUXm9hpi zsWCV)F)*+(bc2#O14J|gF6zj@@c%zlG#oB^4N0veT(k$FmgPTO^g5E-Q*hDS44@4g z4F7t;b6 zX?ThvwiygcQF`DMr6;bZu4k_2t{1MCu2-(tt~Xt8x!!iY<9eVJrT0t^l%iDG*f3L+ z922;Eg|Z{V%$&K=bX74ZO{FX;wZ}6x7L>*#4oy|w^=r~WvpCr2$o~I_%;bRM-Hibr z?@&=tyhB96@eUOQ#XCe49Pdz3P`pD#A@L3wO$4QcXc&_k14Pu3feDu1SR>$~*TA^~q84Nu zL~Re~q&tRPjF2&DZqQjX&~stHsT6Wzk-D(5FuSR^x-q*s)62I!d3^8Q@#XQnWj@LG zcOFv`Ujc~Ez{J47^b))d1++f82D(041KL9Xo#qBwZz-@*gbTD$n27NRE`g15TwjPf zZ-Zg8BopWyI&i&YUk*;Q?0F2LDDHUHYx3X)~UYQ+Pn0l} znI3eXk~xQ_swm$;0a04>2_jbkkjozM?!(JyE5pYEI~Fl#@NnxqS_#V^~>0@!<_5_*#1g#_}M#!WpONG>TW%x$aG%*FsS_r z$`hAa^%-;+8aIo8_91|lZHr&nB+UpK!2nHhYe78#?nA&lprkLoQ3>P$Q8qCa?G2*f zGy>U0Mbx5}4NBmxJR4LlY*4)b+SR3f0o)0MHbg*P1|1FqJ_1C9jonzmT%3(XS<%!O zv|2?_*>JaRNQIwqytf5UtyqvvQp~Ph#+v6C`3rKL{{30@&&y89K#q4e({V-)pXL}- zW&1cAnJCZdf`30s|9uL`6uWQT*IlY;XerKU4O){T^52bl7F#!iJ%c~Pf(-%s8v^w= z1b}_Spuf=pJaXb7=AhcA9>NH1&`HUtW6 zP`CqHjVGWFTB_>-TEpV;$^$eA!s#LDq3L1i;pq|Sk?B$C(djYMW2MJVkCPs}u^R+0 zY;X|RAPHLEyFu;31`mM^mKPW{o3LrKXm93b6M?kV%)ke!K~5%u1Qcj4QI17agpD0? z1|p~n0O=%wa|{b8_lTRBfd^IC*j(&AY`pDrCk6#ftq$U86kE`)8|R~+wP44BYzu2+ z2dBcRL1FWn6GZD}cTF=-_0$H*WOc^%uL;mJ3~8M2<7Ufj>(gweQ5h3Z9ipop(HbM# z(kN~mWaAm-=9Dn8*rU}{yE-DEJ|$O59+MCTpD_q(rh>VS+76z?J+|0tk3fe6VPUVc?F&!4rm=0*afdHs=AS@=VCTu3` zCLAW5CR`@mCOl1eneaB@W5S?F7Je!W8y*9Xpu%zjycY;^H>|tAnTG{53C1JZr8LS!EHmmS}PCPQ@5M$BaY`_ERL4kKy z5w*{D%Pr|w;O%sr(vs4e(w5Sm(vi}c(v{Mk(le!3O7E0DDa{+}Ak9YX8aVLUb6DuW zmc4kjs#(kn50}A-6(q zhujG{^Z`U8P+|vd+GYaxULdp1(82|DN{6V391HUj)A&a38HV~{Q5N17hL(PoE&8VPF^8mn#w^M`+g(PW+PBC1#dRmq`?ImR@-C_rieP|6Le^+4YZ^O z@74#+RVy=rG7LWx=#(&hMr%;<0~w`7EWv`TVFMRH;FEONmDQC+L3?o^y-9NuHDzT{ zVKEoQGyo zoUxU4ZLPT-^OF42Gh+fKpRF%GGb18o#+mYzO&$K;ty?oPx3v2Bv~GF+=@ZiuY4ZT{ zfJ&~*y7t#?7?>HD7A z4LO^TW!{rkTA0;i&X^T46)Ad7&x~^PgGEo5W!SC}8(HuX02zbO8 z5Kx6fekWn4*Uu1*E-zbs*OWng8=9RX+?Hr&<s{V|GPRMP^0jC4cWn zp9qs;R9+OL=)Y}?7~kyPy}N)hAb;1c{C^ujYr77@cf$xVgl*;npIXBQ z+LjDz9tuDU4Y>C}9>-oO3V_a(*v!ognw4V{*vP>Q+A*_{j|Fm+w=%ObBk15WFlH2F zW?u4-?PdN%#%`vFe-jx+|Gj3+|HpO|G^WHVS^3Yr@}DeYG1KI~xu6p)FaLkZ3~I$n zGpI2bF-mWM%#;~Jx0!B$9BgPJutDe!bT5yPz(yHRqfF+N47gDyBPpXPV=3b)6DgA^ zQz_FaGgD@z%ubn;GQ6>yq_P*hg*qy=Fi`!YehN7JKv znewJp{w?xLNS%K?$+gR#sXHttWzjLvNn{Lnn71(|A5^O9Kh z>H-^#K=}*06c03U04o;7#6f5BfM)mAl$F@UjE&4x)j-`1&~PX7Hf8$=V~4-hXZ%(~votKK@~Uyxg~by$#nBjZKmC?_@Ns0ddzrzm6NXh&7I zs39s6mspbZzyOa7%5{7Yk;Rlqn4beUkI{m7uYVy`As|w z8<_Psb274ls&B|4PK=;U6e6&LoR}Q`iZQ*?^=16W#NhYO?{7vSs7ymOffa57=m=Rx zbFc|BU`I4D@qkQVy!VgEmvJNGCcobq;PMk(f`RMdyv$P!!`5G5EhslJv4ip~(I<*-Vq*c#=z)3_rc8=T;N>f@!;Az$+w&9tGgOCk z!sb)o{QGP1FN;<3@1fdm$b>3%CDk@oeFjkmRfc__O&O3AfFUKPy1)j$JDXU++sZbI zffgT#y%GbLn_`k;nqrn>o??+=nPQblOjj&dY*(DFxLk3&;&DY#xu^I{5i}~wY+`1nVy>p5 zZZ5|RYU?X2sWFTIHSOH{V{zm8Nqz?z^UYKA1>BNqeM%-clq><&(b^0Q;PWi_ z8Jst>bAzg2cJR0X2lzBob^-WyP;l1=mZss6&ML4Gv@U=FyrcrW?2(bt6r6S$!Mnza z|LkD&__u@Eqw?=nM%RD4nY1fQnIxFROaHkuy8M0l_XT+O80bO|@SYlOhH&UvBA|ON zKpSbm#S7@33)m)5l&&i*Z-JXx;QAX>Epb6tmR|sEFa#g)0a~UC+TJXv4Bij0WOwJk zCtbUkm#{4P9RtCopp`fOUovk3&rMI+Afpeu{(<2RXr&5h@LlY}1~L7O0eBRW8gR@Xl-YU$TDe7PG zzm$`Vlh0OHgW{d#Un%nvwnYq*4CBak6M{76YG5zzH5);L!&SY)M|& zAPG9v4z#?9O;k`oQAthR6jaZenFu?t!T?@TyyX@V9e2*mp(7uN&N{dt1oZ}({@G?u3<~MX zvbN6Z3kjN-Y1^sk7Gr7_;~GR9(&fOPCSE4qCO%Dk znfNyGW8&azOT@**)x^!j-NeJhLD!auKNV++-Jl|{QHBq?6&KcR5Q-4W5ULRB5Sk&hLTHE32_atap^)+yHYk7^ z5+Y3OqQ;_%f}rEd^_W0258z7~1QkttRZ!Zy{r@HNF6LbfrVKU=-i*>4eDpW?>Tj^Q11dx}G1v%zdILVt=Eo)l z2;W9vgXV<|0sI)hW2vzW7*vzfD-bC`3QbD49S^EBsW&fA=ifsV7i>ipCh zHmC^>Fj#{{SzmgiGK>SRyFft+3Rh!&>5ayqekU)hCX4na23A>+OL#%uOwdUQvH}}L zK*3DRq0R~ry~Y9?byz^`uVvQEZOC?Y>JhmYEEV<GxgnNpQfol-NU zR!Z%ZIw{2qJLUznv6APDgE*V2n5vqpnW~#=m};78nQEKrG}UFQp!11TkEw#rCsKW? z$`lJaBtQaM(1V9xV6hKgR0fKD4)E5gEjP8n=P65rj!o9UdZsdO>?R%#@YX3&@SsHWiJ3erD?HsDaZr?B3sb_{r-d*K$x8F8MVrSFB?oTf6zY5zT` zUmp_G9Hal&Nu9}3zY)9wzTB&17HG}8w@YQYPvuO|GI%eSa>nynA^&U)tt^t-1G0S? zj~Ig1!ZR>2T>Sr%c^fO}E)IEyDVv2kK&M&qfKEo+zyrOo3^Y~G4X#g#oi_lt`(TYg z@URvvkinS`)Y1_Hw{peA#ni>j#oWch#nQ#f#oEQDi!B%1E_PfD)XEimCI)KdG8&62 zGn&}3m>Po)oJCIHprdC0EwT(t53`(T4I0$WO{=fxs|Fu5%PP71&tBisr0|#=H^1tv z_?RTih}3CCLMsk~CeoQe7pH;Ffdk#qX1|#g6ork2nT5v_ZNWLTH+wpEO87R3}O(=Va?Xf9H6c;=zw8HWoC0>M$mr6$Nw&8 z+|F9Y$j6w^{NaxP(^bX4puMJypgmj6OIWoT6d0y&W(J)+2fBSg7<^*^Y=#!R!UN_h zlyk3bflEU{(7fhG;`gI6iYlVE4$=2x zGB5d8jA$hyZ^~p|Qu^m0yrsx&UwYs;cxz_kL{QI;VFL3GW`6Me0|R8(SXqf(9ellj zv5~njxP65z!n~vCs8@umi*aL6O3_iTC|4Il2n$_|>HG;@bKOqsqxu%Q6aM+aIOcj2 zK%<7>HE!S>#KxeA6mzf;1_vrA&9i`yP*P+x76zT?^7|3vrd>b&t@^hLe3hTX?-vYA z415gJm_?Y?8CV&(8Tdit8K6C)jQShc^$iTcLmK={jEv&!pu?$mF}nGFtqu6MhtVbA z$5VescSf&(Uq7HUXl$gHVH)!cW>uJ3toj?c^+6{`fXrfKR5w>QXNHh7{_XJl@xxF5 z-=2UUKLQx<{@dyM>zCiZ9S|CHlO8B#g74yCXW(XVfcO#SEpU?roKHZ7C#dQK7pou+ zXwxn$=xh(r>>&FEP}3QFc%rhYF=!+hgqfG@-ulP(wnIO$oe03pCRQ+QF^D_GN?Wg-seFpst*O zAtTB{COal(1xUBa6g*B0IU3cJjg4^`m!hSCzO^C`kD|4{fu$nXlz$Hx6}&uT86W@a zW|aQ-l(F=mtgHu9clG(nK|zzxS682z5)?G$%-@?#ixo?|AOW-jwP=G0cM#466bAooxZ4w4k8^i=Q%7fZn@-OAV&0Bd1c@23Bc@Oyr`3(6A z`40IR@+;(b$e)nsjRh^Z;1$@Y30`HUDXyumX|Cz68LpYGS+3cxIbCzP=622FpdBIC zG@ogLYZ_SPqO7FOrVI{#==iCqI;6G5E}*O=C}3>F#OLRy@5EQ{vTKu}_A1cf$xHOb z@q5{-0WMTTfh$W73 zMGgZKgE^?{!Ya2sZPQe+M)J$^%ZH{(JG? zA@zXrfRiVgUfy!8jtzOX}*m_XOnf|>n}9i6M#sl#ZaQ5*Zkncd+#^DKS`qhUcLx&lwq*{y$}2!fFn=tBx6RTob}Z(4ar_ z67XG_EI+{4UV_3Mw89s3>bWVyOo)5IV~DV{1~1^Hz)SiC89^)E1i`~pYyum3K{H>x zFTsa*bMQ*=YVca{dhkZ@X7E<2XY%$Zb_aa_4sVixVwZt|fhCSrn?amGnqfb5{8a|jUN;%L1XR$jQC17K*C?mqK@&R1ofMXMu`oMKJj04V0pr#w>Ksph~QP8ZQT^$<* zLD4GsQV?7T2}%fR2wDhw2u28I2v!Jo2+j~(A-F^EgdlG$X!1ww0_e0<32?(%5Z?K ziUZdjuo*RQ{S8W+pfwhdTG_x5x&C6(gO&5(Y8G5uF)=Xw>toIZ+a}3S39%1fEeo$+ zQS1h15_E@v^Aflr0c!f9RBxbK1bpiqthr!{R2?C^8MDrXyB*G8U||3M=KpP$81U^l zpcJppV9a38;LX^&nF}RHoFXG)-xl5_jyDt4dFmm|{05fi48tAf~_N77=SJh`K#&%Pogj z4xj>s(?QZf)4|fg(;?C!)1lI#(_yB=N{5{eCmncWH@FFG@Vx`R=+sbugD0c{1J5DC z>LYMv3TpdkgDwo&%*!arq76ECk{#4;W`D^JvVntLf?b2%g585Xf<1%1g1v)%2Kx&3 z9qcFAd1E&i3T#vbH<}6GD6m0ZV1xMu(5ZH|7eL(vXV6$OqdFrixV;R%JjP5FT)c^j zAdN|a8`{X?syNg$&G>sFjmh@kU43IiLqkJj{eL%-($bTX($kWdlJt#@3=NHp_5WQ5 z@j>ED=k-lM>P$dlP<1SSe%;q(RsI6LqdF|iK6CQq%#0~h{z{<JJ;pUWhor2bm8M2b<3Ty8ViQ zC4<$TfrY_@sk4b;X4R~?|N4&s1I1aPB`QG|_Mh!Hea4Vgzko|gd)K^q&1vx$nx zF)Q}w2_myG&7ZQX->CQ_p1z1akI0ORj{*nP@bGSNmAdH)qd(S zE=I0DY1&nVK?0%z0^$PIyROH0O)K-^6Xp{X5ooRM){gMA{{J893()8R#1~LsLw2Nq zL)8q~*HCeguOZ@KU&D5#fI|-=4mJn0-xIWZgn0=A!83rcR-T|~8s;ycW&sE2Ok40| zIA|w0=pZKe89+?iD*s#r)n}`izcBq_jbLD9-~nfOX8jGUpbCUton4)g>5u8BZ>Efz zESAn^&pR?IGcYpvvFJ1Vu`)0SGw?HP7GwgixnyDoEr`J!5M%a>nq%o^xO%)qMRqs@kgHwJp0?f|y>i90u24n~{2-peuNp^jU%m85kM1vA8jP zU}aTlrG-^>oO+rSWHtGcRtz&i z?jM(g^f%zC3*60s)V|Q#k|l080~5n177nH@tfv`-8RQwPHgj=Ff#%(am@Ht}EDqgw zBMS-+^yT*E;35gUh8nR06tuxpKgdog3bf}Fgjrr11llPp+XWiH_J4x5dRhbofa2=Y zzh2O|G6N@r@@58Jb{1_&UjaNc2CFM!Lt_SptgsD|#!UL4?P|rElR9MRV)n5 zDXit-9uhyeJp#H2NPhzhDC(7kjTx1NjhR;U_U`(7vAcUW3j=60fw7!{fsyIMzgFfy z4C)L#3?`d7xtPIq9;XOc`1?nCWjY z*WbW$XM-8E9RON4sRUYTsq_leevsx=l2p=EvQ+X^id4!}s#NM!nyIu>X{XXj(0*Dm zfsH!gSxX%;9W@;@9XFjYoiv>?oi?3mI?Hsn=^O*?r@gB4R0k9}ocf@K6wT+s=!Ln`c|>SFCTCRO(k+zawUzb*UHnDM~EnC3Xv9Vn7ND^$kt+OZ`f1=fv!+ zFY_x;H`6cu#}d$2& z)5~4Z(~t?@4$mOX^pf!)t15#uL&9b*PS8owT;L8jgZ>5<=wva70b8>RZk2+XL>x@q zEZU$mC54&3Y`H1?5`1wYhp>dOhOmXOhj4^&hH!;&hwu#H6~a4&PY6SHTd=dSv$3%o zn}UwvHizsI7gaVfHWC+OJg6vWE$ON|Iq6^aAANJp;IryRD;4OBbmEr=-%-W#t9S75w4blP|ML<3z zqBFKhff+Q(4q8OPrmO@mtj$bK)YU+{w@s9lM3qg0loflj-Ec-0PZsny=bO!Gen`#mTEW%fdLu!$;W^GzZ4?lCc$hBe2G1e%M-2 zUg+XMST}sL022d?HfTnl3DkfD9Zkpv+CC1s?VYjp-~A|&1TT4K3zgg`rkBx-O2%Hv zGG^We|Fl8-PT(UtA`Hq5Y7DEPr`Rgpfo@e(hpr9-t(Rv9Uy}~nl_$aZWrNHGP+wUI zv<-iQgun)s3!7Bgl|YM$)xZ>Z@B-Aj0gqh6$|`Uf0c-cbW{o$ifH#kbFoDvd2zbFJ z2P5cYc+g>d;!NO6+2t;5kOUp_xj`H}ey$AiDx*3hBf57%Yc7;cg~9iNiyEt&vO`Ct z{@J30gZ#gTF~7d#M@2;)E{6_Q{oRBdHh=a+*?2Sh{W~0EGh_aI&^*rnr_9U1V_)(N z1`Hk0dtmtPfYy!k!Pb!T32fxx{IUUjh9;=HCUOCE-HY;t4a)i(bT5D=Mf5qpz(Yn? zUwWf1){p_O>H!Vo%P@(vXm2)PQh?lo%gyu!G~uswVUr3tfWU)2jKU&#av53xL4zYs zQNTvRO?OJtzbr)F`+FZbjG)2tC*DF=)>OVndrw7&F4X;>(O*Q~(OAAIo9lu3(4 zdou?UbhWj>9Wu&*4Qc`#HJH9^FcsLKe*v`k%kaVm4S@~D7r-NojG!JI{_=rQolzJg z_2Miw7(amrT4RvXF|LAyQ5HNH`|Bi1ie~!X&A`A6x;;^VL6)IogPi^bdC<9G8{|M4 z5OjMY7iewYMnTYm62VvC!8%SsNkL6POF>V;NWo0OO2JOSnSv_?cM6^a4c4&a2Nk4zpt}@7L*bwsIUzSI zf`+mbjf9xb+f?mu{1>ro8RLr1{gtNR`xXEF1K+UN+x~WQ8dF>;lUw$dH;s%8jN5^>cz_5z#s~{jShH`l0O$t4&75q!;AV}0 zqOu_9ZaHHGW)nqaL1QB`=0*Q_zI1keVf@nlZ~B)nTedRI_}BgKF016f&;NS<{bOhb z&HsZ}vcxbbFc>f#f|hNti>fvl32flMvx!lhAC$Xfz|=-XP_iKA_A7A#63&(5fV3oG z9pX)lBH#^NGGJ<>4tR*4@Cq;kLt%3@(7DRu=Hl#fu;tyRkQLwHO)N+YyknrA(0_bA<-c*Lt=%*4v7;Ipru}X(0O|BJ{!p11Mu1s$YN7=LGV&j=4DP5 z8}p{sc6QcI%i2)w(ybj-A2g@Dd`?h(5ct&0W{>25j%pEr&4e7lWXuR&oXE(` z%#vR_L0;SgcKI^nCd1r-fA+U;gSwvv-m;J#alf8}dZEnxpp%wBy|RCx9xwAQ24e;n zhOe8QctC9pW-dPPwcFqmF8E-#81R9w+~xxvHQ*r4<|O8%=49sN<`m|X=2Yg?<}}S| znbS6>V@{x6vC~s0P{)fM;#oa?>5Y1@ga__w!u$trzrr}M9KTtH&6Y)b6F)n6Cz~Gl zHj^zk)nBTEf|^5JLR~}MLfu0>LOnyhLcK$MhWZNi9qK34dBMl}n}Ev)6LAxD6LS-H zlW>!ClX8=Glj$bQO}3jHHvz4SGkIph6uZGzV1w0#4OXBn_Mk2GV#cD-Ks5yoy{UmG zlZ`+};i^HSa}d?A(K(1}&n)__lLf~dZQlqVWlL1)Q8RNn%fFatihW({~e6{7k+ifV+}_d)p$JnDO%?L3nj zgDTkFAl0BT8ixNbSu|NC89+B^XffQ}psl|FvVsP5(xf&l^)Z6aRAB_2aR*u*976TCEoky%-g>1C&W zsdZF;vZ-luf0T8p{w_x6PW>Dk+XN?NWv2w&>KMk8A*pyT&tS0!57GSp4{P5su`#>> z&FzEkH)7ERpJb!M$hkpRe}kU>2GAZLjxQT@VS7=S1wac#n88glCQx0nff?Mv;}O`X z3LbO8x=whT>M_to$ydRPgdw#L%F;-17Z4V{;K4~)z6Z~YfeJYp@I4;v0vol!nOI9) zOI^!c%Uvs6D_yHxt6gil)^e@wTF13OnON(Y7E|md7IrPrs&YZ_@?ql6Bo+|_EguFA zsEA$w?OqWF^&&z0l|Umvh<%@?%BG+J3u8vmL@Mmm0&^DKU0rCmn(XS@^=}W#y(Y{r z|A?bR&&<-@yZ_~a4ju-L^MEcl0gaA>=Pp2X6=d!LT310%x&qf#kdv;!brm@86tjWq zDu`-mT?J7MuB#xbZ^Cn0F*p}PR6}zyR5d6ULsZ`ap9p36ua$*?Rgyu1p?b3b=uYs> zqD0qjplXd9)Y^xX^x#$nEJuM`s<21~FNOt0GA|pbZs7pmS`Ion6Lg)rASezX#V_=p zGVpRe&^(8jF$)8zbp6Z0>;$=QoM|VMVGejm)xcX8cDH#k6R1`L)n|YHwSsSrQD-n< z*t?k-bb`i~TU@WWK$Qn4mn4@amnD}cS0q;^S0z^`*G#UJTsyf=a)EYEb1{R*grJoc zIPt>LD@r{DE-64>(O{E6P$S>kX2`FO4@aR7I(mz^iXzwGv`Y3Ns{qvNKH< zlgtgMogglxt(}8(4b7k1(Cca9m|z#uFo`)q>K;ueS$5FT*jdm^X~6f?G&8=K3BH>K zo<>1+2P7SVO6+3zd2XQc9g>c~8{Ipm+h)-&45&9%P4( zjf0QEf~6>wTOwfHIB;taJQoeiHsDpBptL5y0@}|kCa_T-oYwTk_0{#w_1*Qu_0#ps z_1pEQ>o3>eu74b~>;0PkGkvDmO(J6Ypt4bp`1aE#MLBiwsbZiT9vQ*SbWjgU%-D#) z%`}YCj6w;!7#9$CKMhM9s3rP)O#-27YEV+3CUTmBmWOAV)EGSB;-w5s|H~N|SU~e( z!VKaJGdD=+Z;%AfF))A@tZZcE_yQWG1YaBk?q6|&m#=ULY~Tm4EfNHikQI(%phC?R zbo8(EMm|`H1}?;49F#}{7kr?2Vg&6^*et>ZI-r*c+;)K+AjJq?>dyo^cEG?8v?T++ zPZm_ofOek?Dhe_e@7dG!_hRR+U7bw&U0r`KvUn8!(PWDGyM+0A;l+y=|L$O9`}Yep z5?&6fJ-}y=uLq65%7Xfi;GO0&&{`aPdk+uTQf_d|i5oP~vPm52G9Pe_4D$_2afi}I z2M^+aye7a1T1zTP!gVDA0-$q=c|o4uB+dX@h78(zj&x56w2~E7M!V)`*RIapyHT(D zVSZPzdw0RVGvFIN{{Lt2V_;zRV*{;Ohs@1`*Q~?D!KW!gPIQEvrU;qI2i3L^)zI1& zGV>3vZ6T^5we20i18>KL|yJ0GPi`lY46FP4IVL*mTczZ<6zO=EJTWL zgg|8upTI`YH98!i>6T669H27?K$o41fj5^!S_-fkC2)}k_BE_zL}@k%=u2-D!1Of- zbln`hD-0Up23?5+^(~{asWHOGjIm%3@7lEs+7kvXe)|LQGi&{B(7BqB^Z}}0ATbWE zUqI^uLHoDCvoA6X>7cFZa{3#%?|=&rS#UF1M1P|+*jug<8^EjZVHpa%{13(fXEjhB zVg|Q<1R+=Mi3@BLW%~j;#ZL-cT}WTpAalXMkQKDv)DCiEGLy2BI(U8qy#HL#n32J- z-Xo^iR=o4_&CAt{t)P1$KoPpi(!{H{--Bh&@01_^J~T3NLF1JHG`|D&1L$TfK?W6u z&5+x%HY@SSvS@=gr-2ToU;~dkDG6*)0}WERMr;5#(P8cfM+1xl9-)B6mV&2y|%*s4o21%{&j>2iF0&qaK0!;NUYy7J)^5V4@6+ z;QDb9^AZMb1}_}9eZk@koM>Sjl!6}?o6y_7Kt(qrJ9u1~(U_5W5&AV>%uD`pfv))i zxfE18fk!BG!QluV8UOzuQrd!71o^_nOF`=Yb%RdFU{YfMsRy^$K&rua9fR6-Ak{EY z&^e^VVE=;DvNM)5T>bY7q?!Rd>dvIb08-7)Sk4#&7DrRhaFAga!!C$=(0OAp^$Z8W zYfl*e$AR1qzVF$8GY{+pNY)FW-JI;uHUYSVg5?HSkZocDUxv1c8A5SDC{8f7k%t*_ zbQQa)xuCJ2xhT7!B6IPdeU~qHUcStH>lIIi49C`;1<}($@~S>lI6Rwi61PsNdQb47&3wrFv#hkX?$Zrc2mY) z(0yx{z!yI2JpaXY6g?N}@y@z9*lP1^S`<(|FzjxJw z3h)F=6LqHuLzdFt8(1ZOKW32yi=$iu#}_18K+|P>SGYmazUtA@JPW zCNW`Ya8DLA^9deQg=KGWr3>T0nylb-0rnWEo(1Uv&z6Gx!VK=aQTB@d_* z&A}tVqrqdrmkK#l(^wRAS(_ku z5|pu7(plam##9XXTF1RRqFjFQ@vD2JIU#O%gx=xy_u!W6Qt-8jpmrQ&Z6dTC2ksy4 z0k`8IXJ~`lanO~FpivWuYG^wSq8i+egQ$k|G4KC>$$Xsc6_XkRMD<lqlB=dprjdxF8`aW}k_-T*GOK&oMB8+6ClJaF2Es70jh z4dAp5Qj192pib;O@G0LA)%U?BOa6b!+zfVy39>sN_d$W(5rX6nh-y%e9-ew-Jl>1|x8p1Uk}L@B(Ol&=fQW zztNuO3+Of}FK9EuNMCxR5iG};=u2-jfu%q2tN=^~+@^p{N5K-~CP6OH6u>4!FttHX zV51di*ND|iD{wlrlCaXSvas^7im=MCs<7&?nqjrVYKPScE6A!NJ|@JG_iAeDpr$bR zIwbH($4p3fMuD2j;6Z-KFdyjNsAlt+YB$iT!h{9-B_(zVwT@OvUfSldB{uGT1#T{R zlYHYB>Q+`7hUD0oCU|HYhZS0TO)BwX4DqUrvF4HluS68(2ypLa>~+o$&{edJu&|GE z&=r$352=g_s14Kg3+VXQZJq9^EnymBW$awu<4(|-V*hkOYbror zo}Up0?MebI(U!dcA{C+SCX_wi;9FQ=Nly;>!e($=8dUu#gT2Eiu#p`+<^@`&$!^Z> z&K}O5&R)*m&OV)eIs10@kYn^kKufGZ*VfB{?@j~tt`sh8Py~Azv~1qk z2(&&IGMWk+HVt%F z$$#i7c+hwRWVJhNJOXkH0|WeShU?%`8!XBOs>dLzq4gNV6qdtq)i;n-gX%GeYG^$M zQO(K*SA7$qn%#~`jRB(iHl!Ydss@$)kozERBUFQHJ&0;(tp`oVfXL&=CNjB}N*c#0=>_fydNg=@})RfE(MemI=7C z0@|}B22K$~9e)CuSAfp$2pS8*Dgh9ajU7BV!t|rIUqnbiQ6P72s$r+7ZEi$FzO6`C zeLHmRwx)wD6SG_WwtQx{KRc{jay&e8TCABp{_NQDNgF(H0&bnPGXG;bWG>_`6fTr5R4&vmG+k)9&~~BYLZI9s^h^l6K_3zpkZbcm{s%9)FcmZw z1P252KhR8z9q1N4*b$J3nHKO(dQ1+;m+OJN{{R1f(5b(V!6l&uxCH`hQ-JTq1Ggz6 zz~$yOPBp#B?DFsZqJv4dyLj#vq3!wP>&IOI~%wM5e+sQ z+Jj(VU@m9tV^U*)s0NMVAXI~Tk`UG4SpulppfO~KYEa#PPz{>hfT+F?zBgzWxct^< z5M!9TnSl?qN|iwXJfe)WD-u$vf=9DpX&EJ9gPYv2*ai>n!#Lp47Er=wV1pj`3_7WS zjqMBQ*jFC#a1oKqtyLl2GxRfIn8d;Tw9Cg&>N|?d+G{sAmx6AXD?M-;d;m=AL{Os^ z6q8H9G1&(X?ehQs|AWsMV+GwhGY=ZtO#lD?2cM-3R^5-H8hl0-SoM5_Y6i)F-JrH4 zlNtl4#F$T_FAP$?rMn>52+Z&*pjX)+dLXTfz zatldi65qZ1uh_0#5S#w#@7}!|z79#BNr|x$)M#fc|JMUf`|;pjD@eQ*BL4Rhinuak zIY@+oiLo6d&L9rjDaEGGq|8|UuM0GP&)5kP{~rzRsesG@g*-E3`QLL8aqymfkoXj+ z`3UhnZ2C+}40Dmi!FOye0riU+mxE%8u>xerfAHxstdb1u3|tHbkR*)~K(G-HaK{ne z_TXTK9?S&370y_gnVI=9^!B$+yJ~OW2F>}fO8&VDS$D(?&V8U&l6(x(3@Qxmn-zIL zCuec<@Uv)x#vx?5zHDUR`LcwV4@oq7EdS!9fdiFf3?6i~pEFjuQr*WTR}zT>Mw8 zlU1_xcMNF08x~v443wUp1Q|6f2H%EayO|3-_QfTz0p;8ZnA^Y(1qCKM?Dz^6Ht-?s zTp))kBF*B1#<>tv_pteTQ0oyi3RDgrX$!@A!{#PtHc;Gy774KOf(_^9g&yY!UNVC2 zmJRFz8<|mDVaf<@*NTGA$Swb~i)q5&r=`q>f6JLPOG{bw{~ci5TKfAUZlkD*1N@lLrGcWaI+8KUZNIHxHsQvz+; zW{HJv&4zdmJP!>EV_3s$Gb6|gpxw~Ko>#Vk6FdR!AkD5WuCA_buI{cLuAZ)5uHLRb zU46OwcJ`S$%~7F~uh?O~v! zq|4}r1`=(BxTqE)* zVGZNJtVT&l*hl2&t@!8H1xiX=|1mJt&-y3LD*3mSsrv6R21a<94!{2xT)1Gh1lAPU z#K-^|N;WW5WMotZjra%rYyQ_dnQ_xD#!W1}zl&KVe?5nmWT1S*!;rgyS09uzq4|av zx@%}78_$D`A-EcSonnHK&&$SV0ajA<3DoB_FzlOYLo6fZYuu6zR*bi2+*W*%_S z!VYefyGCq4sWVWVisCw0ZL*1pffcl1gdI$QoP`+OUG?`Pctn>a06vP#3_pXLodI;x zUL9z2fT;ck5$Ha)jf~vj+qK#5Y-HyC0%~=DiyP3C7&qvy-wm*xUnq@CSWs;i1kbLq z32b1v11dH_$rWORUdp>;3Mb_fDCs)+g%VR z9)rp!5>7l8$9`iG=mb)^V{)JqNWqsDK~fP)!x|iEp#BjFryg%$71*c@?m8)pE2}G; zE4wR)E2k@$E4M38S6;5XUHP~&sOzNsOc_)#L6*b7)(3)agn;bMhYTs$F)<$}Tpo23 zE{$Nd5IckS1`g0h@D0%1#DP=`K>`3A=~#0PIB$bOgG~TbVu2DRxWZ9nE(WbGVRm9! z@;ioA60}nqWFM#%#>F7aPy|hj40k}uk?q0;HvNsPkleuy+UY^eMio}jS{jJ^Q0iz{ z1Z@@sO^<+TYCcdy0n*$8-2x@Xj%Z-Lx%%&BC*!*7Z#q}?{kzGi-nWW*$=)4*FS1JR z-ql|DkD(Hjs|VMK`uq&ppi_-`FM!HG&O4hNI2mI%Fy7hZz{nW8k%9LMs1{`503E)z zkq6wCfgkt}9_@lfBREb$F~6gnoU_uSxwnY*-bf2IZe4txlMVR@-pRZ%Ev&t z{i^a)Wu{n=*=o=+OO)_{MGs_)8oQ{nDdcu$(Dj>g%%FWN>Vjrwpj&YS`Iwpa*yhiU z>x!G1Z*84FGp;LccD`+=x_h>B`_#Wq6Wg6L-Bp?TlUCFRaIIO#6;Qh(DS1VsKkuSN zTtW4#lHF>P9C-e!iXHFmJub#{o5v}kmVxpAO9lq!UEoz;RgkDdNjxa?ktmfFI4goi z7eEyP^My?;;0roH+tJuSXJ~-V6@{!=0G&f6cwv(eL|*s;XslTL0_ezRF7QHd=?jqa zHkg^gqYR8lkt?bUS~a-qUth%8^AZ1g>lvm0y~<@Q%{_fIld-h$-v>sC`clR<|3XSj znPixROH2R7F)k`C{rmRs6Hsmjl|!I9TACqrGY=1F4HXOI4n_{p;XACLbEQBN->^$l zP}tj-eh%hiQP674I8JPaRWL^d8fir@}M3_K_O@QVK|G#8z1oyuegZtm* zV6{A;8E>%YQY6t`|6ek_1dBrY>JYV{5qL-seiK+NxCalhjd>S?FoPz;navD5BH(i@ z7(g2=%oyAl!WhyR${5-hrZFsI*v4>-0kmU>;VA=CEF@8Z7w*6Yrcl-ef*1RMiYHJP zM^#^Xqbev-v4Sg6R&iE!R&!Q&)^OHz)^gT%*6FOvS+}zuX9ZQFtj}1PVnM5E`9bqa z{4e>z^Gf^@{2Kfg{2u%f{2BZe{2lx=_*d}n;6K678@owHRZ>vMa+91Sc*CI*m}g)p z3>~~R*JB19vCj@Y|6ACYSy-7FGQ`_0ZJp?B6XR|oB4XkmW8<7?E#0Y`Fe#;ao2JF& z=H|&3n%k;VCMD=HUUq2CadXRRb+B*Aa&yaSv5(u(9yGg?k?)`WKV3$?&N)F{8=(92 zR?hz%%BWpL7lO_;(m`(_o; zRU4r30%>rZNQ+CWOPfo(ONUFROP5QxOHY?xF1=m)xHKqEq@PJM#cpC|0Ie0+B&?_c zvXff?w83)AO`VrI;26@8(9zJb(DBfT(8ZzK0!(K3(^v27u(qt z&rU3R7b9tG?xd+5;;gBtnWQLY{a3tbuezZQ;Uc^P)!3WF&mk8F&HrH-z>wU z&7uuDb`5-Py@NEHjF^m?jG2s^OqfiXOqooZ%ru#0GTUU1$$*MTnWr*Lv7jwO;JI5! zSqW|-!4edBHXSr52D$5+6+GR#g_w0Spo0U5J-l?2o|q_jjSb`oCRkX4J0ixSpkfhp z_z==8rwQmDXdb8Xg^4py+NNpFD(hP;=aA%d?wm`Ci}JQ9)l2jf?9VSqSyk&%7?T{w zw8TEsT|Fhm%`GItOVU_Vx@O9oGdNQO*?N`_8`nG7o#b~2n~;Ee?x*8uJ)ZsO+x zofrZ-XVgKOO+rjUO~OpVO(IMpO`=SqO=6nFGKp;x$0R@zAn{ZJR7a^orp~}?YDMiL zH}Zpe609bAur(c^`zuA+SRe^Y9JKeFja}W$jG0N%I>J0;@4U2>buXI#Su?%b@$cr@ zT#w?(UhXYP)+MDj5iZ55+LAmhY|1Hn7i65?GQ+gA)NsL`(=mm;0rG5YqQ((kVKX%| z6v1aWfoo~zT@11e6QSoO$bnlapygs5Up6rag3En*feq~7MOu*L0v{$}l!z5r8^BZhpj!*YLOYPZU3{WT-8;LRM%Qt7t zTF@01mC@U!tE{9bEov$2p3BVUm$u`;A5Z4(B@-4*`um3IjkZ32HWyz6=xk%n|1X*A z!M%Ms22F+~pvcw+kE3XTdjVYfph`!Qq#w+YWRN zgo8A@h`5Nlh`ETnNVrJ4NV!P6$aInABHKldi}1#7xhC>VgeewuV!Avi9Edo)9Mt54 z6+Dm|&>4;Fn9LRBn1z-2nAjEdn3?Mh(-#*O&(GA?&zxUeyf9P$Ux;6hi#*HU15Ao8 zS$=+5E{aS{4lIf;Sxntie{4!l-t=SYlpkACQa1hiYwy>&Hz#Lrryomi_S ze>s0U|8)N4{M-4D^Mm})|BRn0c9S48H#q03LYg|J;3b(Npu^!_idkyw2HB}zW{k}; zlxE`Ll?Po$7E}3GyHF5R>VewV1GFBPf${%C1_tJXtojT>4A3SfO1}kMLxVyD)T%@p zeFSeC0G*}-igJ#Z9H1hFgF}KtgTsQugCl|?gQJ3@gJTBA3XUBdCpbVY67Wc+I%r26 zD93~98AfK=f7||TSj4!%QrOrgNA>P@U>!%;$sM@V5YbYx% zYN+awGtVc}&hno&tK{F=S>>93#qLa^f5mk51#@|LBSCE|1_p53ii^Pz;#+WJ!O}8H z?FaMVCU$1fSTE@8LMC?5z%jTnrMzig_rK;PjQZvELd;7({8PNjyyVZrql}F=89N8j|9NsL;1G?v2oktIJX_N(MKbnIyo2;0unyi_un{1eDnrxYDo9r~%WwP63 zkAY64xGMV;bRq?_1^7e?X8nyqpkYTMuH@dJF0jGo4g;tx1H}n!taq~#voZLBK5>u> z#KCQG4sa&ja+8Rji-94d8uau4MAa;246B*tm=HCRDXh*iW0cM>b}5`3;J3tH+pWs2 zbY^$&w{JeR5&CY$lYQW|6R7?wJ=_~Gg=vHGvIM{S2t#G%EMl*0NFb&oi6zZgSd zc@MZ>sLp_E%oa4#N7FG|l*9wd_5(L&y9aa?RPL5W z@a0hXTN+(E(a-N=JP*45$)kQ#Zr1ijPtV5fSpn^7pqrlp0@{--ERx!xJ;Wv8P*r0% z2R%ED;SMNuA$A&)a{nKPzy>+wiW3%K-~JC53TkJ8>QG5g zfl9)?fJnVO&=?oEi75(M%g(&Ssd7W!wCYa7%=!75t128iHG?YsY_ol|ZQA`pYD2(B zwlsPq{&Qq@i`mc_;@-S9E1*5aN;9f2<(H#{73jo6X#Qv3#h}fw9y-&h1Dy{6ok}2m z2Q=IWIu=L(bP|<=G@GE9pqikWpqpTrV47f=V4L7H!DWKm1dj=VT8V;B1;Ho$gQh&U z+#=#INd{2+4O9Yv!&KNVawD;gEdxW8HaL9!u&4;A{xO5*Zzhk%ExEmCtrN5tWdt=x z8=|y0L3uj=;KUHdi#b~x-E(4`T!O4p+5_Th{XpBl;Vn)`R_3tCg6HF3%u5)=7&IBq zg32##eb8xR;C>0HJHvWs69YeJS_%}~Qs6Tiq{O7uq|Bt;q{5`qq{^h)q^3zNliDVA zObQfaQctCrVnOpEDxlo2@=66%)pDvxs%WZMs(7kIs${BEs&uN%R9UIAQ{|)zFSHg% zsqDZ*a-i%iBCruORxA!a=AYP34d_TeP0$)mP;(ur-KHq2s02Rk7461) z?MYU~5w%`6WtjmYpt|kju1p>=+;D!pwSD+nh@TFOh zMu~$oJBK)jI)^!jJ4ZN2I!8H2JI8d6*nQg=WErM{QmDK>Xw4++1<+Ci z(BV+>0vjanfVO-Pb@nBs_C{HA1|I)_c@$i)gUSN$iQ3Gvu1KkC~csXuXo=vB&Pr7AHt&g&*cB`hg zY2gNOgM{7$CcD3fY?EA+{i-LsWbBPGO}Jehw|{0L==`yN-OT%#cQGh3=rU~IEXgCp zqP>}$g9mhXERQO9EJadGQccoK(oHf-%R?(dD?_V7t3zvs)(Wj1S|_wXEl@@y&`1~` zc(55X+5ui`EGi-$jPM42s#VTn30j0 z=ikYH$Cohn{_Cw|^kURq^skXI?q4aB_TSe`%}klDe-ARL|IPZl4xCy+qt+~O4AKnV zghud%ARTyci2-vPc-$VwfemhgI&t8jBxc;6@gjKKelBA&c-;QtKV8VU{jWc4ka7FV z42<9#&9*UbV=!U3wpo}5w7XdbG{dl2mq!WI+LM$K6tZ-XW+Qx2BT+RAsP=-5-lDAZ z)zFvTr~&gWtl1}0MB?pVh}tJvJE`r0TyKd z^>2$oD}Nc!gV&;hPVg^fVEkYH|0Q_6x-!Fy4J!Je<3Lrwrj!9uz~Z= zCKg3-KTHrzZG>!8M?Yg5rM!d<(SRcjWnKhcWiWu}a5u5=fx1JR1i=(&DV7Mh#YV(% z2k10SP{||p5`2{_hm?erhLnYrhg5`AhE#=Ahtv$I6;eB-PJpg*WdfCW8$d_mGK1D~ zK(^X}DpF9HY;0sIh}ejy+{yS4e7J(DU65{wniQY1Y!-AM-Y(GkJTcG_3;!mVhgr!> z=5lfrG8WW!xs^^2`WMHt1QfHNuwDXMlg)S*9M;{S+XWdI!LbOsR8io7g$-DP3j)xH zsuG(ri}ogA7Vw~&fWSr>P`JpvlmVB>G7>TxG8Qr(G7&NvG8Hl%GBadW$n20gAp^dm z88oBeAk9w9k&%pS?4XSyERbV%#Ke(yhJh}G5)%`bV>UJtW?o}lQnDd>Vu8Q0xLNuB z&dSwVrKP4(Rjvtbq1voWZhv<%E31i`yN8>8Nni|Ho|0q|T{mrc1ZWLfSj3EfA#PPs z#vxrBqDv!IyQ@1HNHeN4FhX`JvFbC}GhE)x!==EYy;+M}lSO-jrocu9jxV6?#~c?x zgQhwcHYf;eG~@XKYVkQjYc7=Qt+n-~H)_KQ7w{|{EI7eK6`4`cH>e71Gz7(h z;Y&ksEEq}{Y8YA=dKg9+W*Al&b{NhuTw%Dw@Pr|_UWA?|BdRQD44uORx8^`~BxraJ zC9FY1+2CDxE=5xU@^+Y!v;!}wHNhk(fYgn5jF7$Q;PrVlTn7lBIqqRnV}P7A0V=7A z85kjF4}kZ4`)p5dyEg%P9|=EpB1!D7IeWN>_iE0>VqXk@aR1( zDS~I5VH|Kv0F>vs!ApfW!J8j8$w`3<=nYcfb=nTn?26)w>Wb!y?uy}x>5Aow?TXVC zmn&{pJgx|8pejC71pD1YO&NUpEu)~Zkr?O#9ni73(3EHn>cE-`G7IIz`1r)+{CnfI zaP#KH7k4XI`DkmHiAnG(NX9W1*0=s+sAp0AdtuL>-P>4}{Lu!Likw`6 z1J-Z@@iEu-Bk{2A>6=2VVqV244kV2j2|7 z6?{ARPVgb!8)smMs1PCZA&~9p@LI47qwf2A5V8N9WeKR&!oa{h4_xD2LDYEv|NnpZ z|0VN5wrVCd#=Br~(7wD<(3;qP2f?Ak%@Dkqg9miWBNJ$4@n#0dt`jCm`xoqeSXiJ$ zGdPsM-gk}I0ILJRt2a5p3veJCH9?y-nT!7|>H4<>w0`{GK}LJdS?6z%FC>hpmk<&AB^ro zp=cKh1H-?A%u84$8Tc6_7=kwQa)a)k6#%Dv4)CqfT->00LqLnj?l6EBVe?_h0}8W!$hRDQ_l^gHIC52O9RgREj3{(ZVQ%aTPO+)oAV!GhKEwBF+a*&7KyHM)MY zFsMcVt>oqapZ3HBP46hh0Js!{l?mVgfpJjE1Xx1=-1`8}u7VnJOpuN|^ynFG2u~19 z85k-GsxyL?=rIZ!3o;smmh`g=GWMGP`_&H$qp6JMU5w^aL1jq)zhCA|Y$<=IF+F6m z_`8S61&mMpm1oLI`TL83kwNHs_IN(cWM#uu+8b3%Iqa z3$Dw!^|#!Te+9m0np0jx_)BMP*p$rN-9H0YKW zB{p_8Hc+!x6m)+qX!M7TU5=S~hFj^(khmqaejKelu`%7&8GgD(iG4B74Jme>o+XZz zS{i1xr4n8d2?imPvh7-GTr=HO7&F3pQ!I?rrX>qa^YzhiOR@?`wG=58GmCW2b8Cya z1s*K|Co5J^tZZTjQwD~@p!6fCEXb&A*va_gVAtPIs~|_z{CW=B=m6^>f={buyacZo zi$V1ws2&C9Juk>gI?$Re8UjIcTR1tnCTzk+XOq z$NJ#kdBe>x@O$c*;Ct$X8F0^jf~JmXI{OJrRIsuPG($t|eB6f79V>`7=aAd6^8d~M zH_V{%F;#}g8`Shc6U9QX@i8^%!jcU_0viQ+!MAU72yB!C&)gAlUy>kbsnrHm=m;9P zYYj_h;9+MN2VB^LigW?U;4-hkMnu}%#vv&osUgW6>mX_&=^+^*nITyr*&#VYa)sm$ z$rF-Xv7qHCBy5<1u2BLn55^eYfsCTP*wDGJ($utaUuWeSP0$cpwM$lah_Z4>cNS>m zY{j8G=+;f}u$gOhlrd!Q=G;_9RZs~C+RP4Vbq?m;H{f0(t0aRELllAarMG&lV zBYJ>mlOTA1Jm?NEP{Eh+TUC=B7 zXvdE*Lmadu071J9j}+~BhqH}Qk-y}^=sz>~DF%mXg!Kw|~0Y=WTsV|YQ0 zF>reoH1`Er6sIi6q^uxl2s_@U3v|{Et0eePo01tpjEjCfXNvpJz*r4(A*^)aW~c$3 z2*7?}1L$giJDc7!Fvf0jU|@^|9}mDFuz~r`ruWPsF=obC&~9hY_C_wAFQ6S~pg;r- zwxcvC!MPmfYH%|f#sT*QL9_6njatx+<&2P!R0JJT#n{`$$kO>IrjwBc5|+Q7gJ`g8 zq4CMjkPkUgVlz7jaKC}O7d#*TJn1GM)GFzR`Pc8&g5Om zyOZ}MFKj6a*mWrG1|;L%TX&w%@6;Po32pMh@C;1t*>0QQnGY=B!4;UQx|<<9m+ zn>H=^_lfa#PHbmqY!0JCXD7^C(9tj!)nCt{FgG&;?OcHDYy%G{fL8H?1{C;Ml$F%g)bkU*n1;KX zi>fIz|J}vp#;P6Kmf%_yWm;OQwYsu%f4P~sv46qD=nW+$#*D51Zb};fB9pkp^EX&R+qftA$YGz~6gK!MH71{x#e1TVEEYCSxtKZIz% zI!Hqru-F=}peabueXv4`3W5TP3e3ua%BtW29KfZbIA3Q1w`!HEpu$sZp54IJQe<_!!P zL9?x9#-J_Z%8bgwB4Wmhf{IFPjI}}Csg`f9Gp_48^{;u|>}3-eRsP+Xu#Cx+%_62b zXxHvv&zbc9USwXECV9xLsamy=s7ZvE5RvCy-e3PgtxJzRyW~ydtX6j}dW}0SNX4+;t&2*XRHq&FKpmkiPPfbC)d_Ywbcv1^C zlZ`S-sG={uQ3d2xRyG3`?G57Kf^f@CftLcH`hr71LO?^nLcl{HLLft+LZCxnhQJDe z9Reo=cw;xotAG!FQX&3W8qjsSx)(O+>KhoUgS#_Wu7wj+hTI#+3QAUp>*1Ji-3HeM zy)BNh9QjtbKR7Odn+LfgP7?JlI8c2GUQeUXz{_C8$hE;*e}j$w25aaMK^rx}sY_E# zQ%%!M(@ir>GflHhvrTiF<}%G~n#VLjfvx#e6Lf4QxTv%d*kBC4+F;8qj#uE_7MvWC z9GV=K9G)DJ9GM)I9Gx69IaYG)XriYGmA z=C%NjlQ~GUGmA5;Gn+HJGlw&$GnX^BGf!t;&b*!ZI5VguWq!uY6bm|YKm}Bis=Nd* zN99nFP|;AaQ1MWSP{~lKQ0Y*ap|V0{hsp^R-q?*2OkXz2vwhj5X#v`&0oo@F9jR4Q zR$_z28Ds$_Xn_ZlGU!To*mx~yl?>CgHPhY7!}M3oS~ho@eet{mT{}Zvc}+`AckL2m zbt^+-W!vlkT}h9K6vLzy)oyOpE0SKnW85=;zhP{n|APJd^{4&Vlp-W4B%>IlX)Yuw zjBi}1?|EC-L|X7uyV69q}>hLHM->%5%=qEk`n}N z0NOz8Zeq{@YJA`h;s#~V>64JE4qOz&auuurzgZHrb$An>B52z+s9+Ui`vUFogI3|O zfe$|eRYq*=Vq)0OMLWLhp8(@Um(~m?x3t=rZFi>2ydT55YXzI3={F}{~TGGbI z)!xj!MAS6S$0JEfwvf?F+fD+RL&HsFhOR*Kow6x1#U9mk;znkN9wN$H0S?B!dz|H6((-?LV0Puo8Z= z68L5bE)sH(h`>f-Pw6r+6b7w_054YsFHwe;bE4qgRD4YAozQE<+z$02UoSQhcBvTS zBJk~DN$JQJij_ic5@TTe|AB#lc?)>0b^T^G&|EmUb94c8PXRx)UIX_-V7>;&FN_1; zM+M`+!UNoP0I!DtCj-}r4d97m(C8L37b9o_hYdpUKqx^l1sW&;9lZ~k0t0XNR%cdb zv4ya!ky{@kud-9<{4d7M!%4TV% z*`A!4xUk%bX^Bg@fKa)IbGDaya*C6OPgH=kxwdpktg%-_h?`qVin@CysP76Y7mJyf zFpw~R1q&r``32*EOEy??0JoDs$$<$xafNUGih0Rj0j3XsVwi>gyo1kQfzDNC`~+HK z%5W2M_A#jMIxu&tFfjfH&t8FZ5OK3turL8&DhzKx%hA=qyXaM6 zLtvadUpDYufMrEeb`5e0Y>QHn^`1-qZpc)dh{{DS#)d6~qutQ`OGI9gbxv_toEfll_&V7LxFk3s}CzorSBUlS47D9rr@w6usr0JH``(krBQNS~1A1<$Fef`eaGTvc7w zT-9AQTs2*_T(w5e=Rc*}CjtX!PsIJ!a%drFP#Waj<4$4@hy&)zW+(_Hn=qaLUsowst+b+#Z z(<%jYIdPIzfD5SX1CMipTh0{CtieJERwjeCKY)g|*#tI{ey$zk8%F29ZlF`k|LtYm z0cwMR&x~hY!XU(;3~4!o3t5<@u#~@A;3NvG6yZBmgu!E(TW&JFWCEv6CJ81DCJQDHrU<4CrV6GG zrWs5tn07FoVB(G4z$*ai^bobpaT7N$_>fjnVJ1-1TLMBUfdx0HgKq)R1ua1awIadC z)0m60DT8k#Fg606#byRwpsl1Xs4NV=a2B**g`Kh2GQ`W%(<{W%G@`$+uRp@H3(n|* zNijW-Uvu*0s<_&)KW|?D3uD~+&pYhj>ooaPDQYg19v_^r6d#c7hDJDv3%JeaA5-}2j?+^Pel|r2UE<9#>~6^ z)d%g^=KZf_A|u1McE+VRsQo}v}a!O=OVNIACwjxSu=Pr>rv_|Sd!h0YX%Rr zAqUG09`lla*K0dnOJ@Xi|A}D&UDr?zst-VCtb@a+hC1_iFgJr+Z!iwHn*rm%$~n-n ztDsXAxj|_j`}`d=Bx67{xWt9VCO<1lo(mgz z^zqE%fgA{K!07+E3Q7W?QAcP`h$gdmD6R*!kx81xySr%Jx&{AcykVBijJbb5CX;!| zzwXwFliHc}|Gx4K`L{a^G!6%=-Iy8p7(lalBH;U4I6!v~YzEKhfeyfBxj@{l1B{@3 z*pMOx9L6yBgA+511Kv~)D^kE|9^^At@Nv!Du+{9wg3650a!^sw7(SE7EZy1pH*dkL zg$rl=yV}VZ6ce&&Q3xpA#4zcDy|(0UDa32g)ltk#7(nxr`OtYh5oqIQqX6hI-wU8= zN0vLF`=N=NtAzLrB`Lv*#LbN0+icmvgYO20!th1-V7G&BOW4Jjy6YrpV6c-Z9ySJu z?rKH`WzZ-#_+~@s2pBkoU||O?n?b?F$Oc+w%nfckY`Mwuk_B9Yu}H9JuvoBoutczA zuvD;gu*_gt!Lozp1PgEM22t=%K?iAe8F3kP8FLwTnQ)nOnR1zSndvgiWwy&42OaEo zP3D;lsCC7t2s=3l+&%?uWJe5zm@6`hGyWCXG9_b?b^^4C<cCM53Pxt|P&{a!jqwZ7^K8P5U$)#7eklwNa$yN!4Pgsm58(*m z4B-ml4&fQXD};9lpAg16&n76KtRyI4*tu7Tk%dwG-^+hL`1W=(#q{;{GB5d;!MN_9 z_&*88_5U&=7%Lge|8@Rrhs~uiFJa(i5M!7K+EyX~zK)am!UksWRwIr(;2Wly!56sm z>VrnA`M^_4d}4fRd}e%Zd|`ZPd}VxXeAD=r@onQf20A(ED&JGk$w{1`<-U+)4KA-? z=?q+w!$J!-QnFbPJnRZN?3fL_7Y%yC50;6vsAZdu?_&JWb^qU*L(^MJ8B_kvDQ#se z< z)?(j{bU2!5Uc?f&dw1!-i=f@d zf2GaCtmGteIXDZMp2Bt?Grxq5%`E}lYsvT)9B$p9a0B0D1M2%J;2%Sm14kx_W9Y>8 z<*<*T!w!LBR}}5$-!Th&@iAqaC=(mwqP}ebE>4i%8T>SuRjr_X?BM%3nRhW5G2Gl> ztiQnoG-|Ms541#v?-h7294DV7pC+FrpC?}=UnXBAUnk#8zLk7C`A&lN!WqNXMQHMT zaS&(2IwJ@=Wnc4{Cg_xX@T4FlgMcSpVHpHmwt$+#Jm4NHk2sGyk2#M!PdHCHPdQIJ z&vc&UJllDW^MD2nc%Jbv#cq%l*huUg9%6_9H1?BJApy>rN=|=O5eoD20q=KQ6Id$g-2i`I|q1u7i`lS?#u>mEP?_JbeR_yxJL!b zGCUVH@PZnLDEFE(>F?SFxxqa464GtuuwBET(t_bCIMstn3nm7Zf2B+>StS{m8N?Xu zHZyQAf|k88f|k8);MWIj#Dqj5isQhuB%qth!M$S8f@AO!BG9E)j7pI8qoAd7pnKLu z*qC1K+V!vaTT-S<66kaL3Q~0DXFZd{Waqx;g z*nVK}cs?vdz&R4e0k;ES!2|ByfgJpA3Q<@o=N%(7KP3vf%XT3d!_{U2g4#4 zB}7nS8eYJFZg(NDKN!-NV}Oi%FJZmRz|P9@y|tO_?aY&K<}gx{qIF&k_n*d>sAieVuG zItP0lSR8y$F#}W_vSXF)ERz}oBl9EOG+77KzA>(xUoty za54xoNHWMW3TZxLE>p;@Fm3Jg;~_EniL^NghoeOCC?2NS;id zN}f)hnLI0bcJiDAHFAhr5Gev)b1DVywMbt89g`pr?YDsoZ&(8z-1&fUz{9<;e1MYK zVWV~6T2Dq_dZP@eEaYMoV$p`*L?y%pIyg#9U?U%>&qmBy2@C=orP#h~0N?Hby;Doj zn9<18L|I8xL>#TD{2e))GdXqt4{sZ}p`i%PjF8;gtyc9gTMKE?4|6gWcV!p@}z`)9&z_5W8Qu2UnJ6N3t z^SptfDmx>iIwSMNfPX>%{QQ_00{j>^{0s170_PGJ1~(Q-7D)!sd9tnyo(v%j@r+8F zBe^74v^U#xxv^+(_T&;~(cY}i1zI|&4(j)8a1+=7zVK*+CwQ<#0=zz(UmwJky8x0^ zxUj)qV1xFB4chu!ZW+8X04ELuNdrv-O9M}XNP|p+N`p>=nFcEjb{d>C;Ee^THoLIF zOdqs(&>Fnq+v|dZG+U%tq*|m|q+4WIWLjidWLxC4$Yqh+B9BFaR^vrJjbw`55Gk-B z;KGIg{SCqb8$vID&Jj(#!?4+ziJ3)vvkDXFY+5%aPt)St#0CJ(wg$*j; zd$&VC7u|qHu(ZJ9Iu|yC2y8IF02(r|y8se*zp%krV1qAMB=o`tXMqhdpr)t@ctqY* z8FWwzWN6;l6gEaLCMs%dY62UjR~9uk5@QB+>)1p^jg8Dehf67ghxN@=O_fEVG9qHe z#)6>3?m*iw%uHD%^Zop?UgY`t<>mP$C4fl3oM$7r(UzK;<_VgXmYQ*< znwpmXo;-^(*937Bl0c-UB_k6^PXeaSd_RyuOgtcE3Fd##LyUR+>>1+%kdUVNzch$R z3=GWBRCb(!nSq^wmjSwf5v4H+9()32D`wa-S{Cp^B{onXDVi#>Dw--XLK5Jje^(i` z|8|2&rX{e%$P`vsSopUSo+Lr3Qk5l!={SQRgD8VE!)8H{xtlo|*;uqe?GT|08#o0v zh+Hr*R8>||XNKIIAuc8+4hla-Q$?1TMc1oWrEOWVAGqS6*tDCEvi<^tHvxNOE{@2J@&-niDlP6CYnF4BS{lF-omZ|%1 zDXZk)A}0CYM;Yh)*46pe)&_tOIL9+Eu*5LxGjKDwfkxkX^*1oy0i9ySa$y6D{zeW? zu)Um|&W6Wi2WXwBw z@L=A71EAUUJ^%l)Brz{ykY~_fm<62!X1KFalVg94wT1uKxj=@qt$UgHOUTmt$sSXB1TyR1{TaN%E{;lN``r8Ybo`?3`Tb z8Z;rxR>LRLUd*dH#`s@Xz`y={#yNKxFDI?3^)ig8^%1eo@X@x-><_W8Om!Fb6bUPx znBd1)@R)G{=zM_xkN@9f31O~f5M_|t%)luGN);UXn?*rEy_uJh6}(3Va%~sLWs2hL zjEv&!ilCLups)@JnRcr5U-y1L(49zr`x#4P7F4?a^I}qE<|{ZkBjSJ$qZ6a6-#)jB z1u-Do7(h#1K{3zH-~eq~GThm~0UyTV`~upM!wQRec=g6~2PDqI_yu&SCfkJ#?2w~w zz!yz1$NYJd$7EK(sDI!9sIF)7V2NY82)^@qGb1|#Xm(f7R8f>Aj>)65lYxPevG-p$ zlQ>&912co`WkwKzAexDl_&nUHrSVi|JzLA7_w@K=v_RWPs>p0fjJV0XsO~LG;2LDX1*S zB;Lj3(fNB9^InikL4INDW?*FyLb!A@GdpC;+E@@&>-Kgrx&PH?>;B``39^gH1MD}D z4pt6u9%t4^3I@;tAH5(On0NgK+wiZOC629|ft^7WVgqPD2l!TBNF59AR)ZXgVkyXr zAZvGaf$c^35^557v;k%k8^k1VT!Ktug$9+fpdx62ER%TGzhcJTu7BOkdpntTf&2h+ z6VpWob_S)*tQ>5x@B(KokOAOG1r-C#7YqzRQ489%z!C=vagWZ?GY#?4^2nHjoQ5K<*=WZ(ne zJ<1L0wL?lkcuMAA=VZ~|%)rVBDK%I@m7*vpc0gw@DT{*Rh_RG0<}#Cb*PnmPJ&Yxt zuR(Ej85-l<44%;XfcXx1Oq&NB>>T>wOBGlkp$`grn2li1fTx^5@c<6H&0OrP&=3?< z217+bmbib#oo_lBOTk%^$>Z-1ri+ZF(69vilNG5h;sKAtFzJKON8tq>Rko1}6qX>1 z!Sy3-6$Ly2FoTMj&1|gfV2eRpTxFR+-e!#HWbXb~+{w)JyOWin^EKGhko3hQ&J0Rl z)|=V+7+JJ8b8>(-ld%hIWa0r;ej8YzBOh=p*w{f$JVsUq$Oc?c0Vrr{465@4LHjB% zUuKNi-No43^}6$4&z}-zCa5hJnJzLQLKP)A87_bhl!F8`YCNKcDkxH!prI-V@)tN< zO&MbtOFQ3mUS_)Zuluh)C{&sEc7kSrLFtnjRBCZEIBjO*1ML_CpCARA$zlZE%eavP z7M|daJS;rn5eW-VPH=dFiV1MfL)jE$3DZT8rOZs7zZsapUhM*vl%Tu>&aozt9J`s5 z2NYBc0voyczJS{AoRIX&tiPEP6o{L_#So}E0R<%^XyYfSb`>=iWoG)<-Nod7`7&cK zV<~gzUxv%gz5a>-At~3Ji5W@5$X?6dNhLC!vpSxFzAD?BZj0$HcTh7z-^>j54JSJrD6E79L6rjN7$-&1%bmUe{QKS01@#OlzcDcOf@=wG z2EEN3pjra59~iV53{-wVi#bmH4XpZ`nL))hES!X)2~JT^*;G;zTxu_3l6=|8%%k)2Ppr!}|Gm_n$4Ezicp!I2j`Wpmb4KR@Hg5YbZ zK(@1kHwm(Uwv2&m5@^30R+EE^3Ge_c$PHYqpnT2_ZJQ_y8iUg!G$_Ee3&J(!Xd%J? zY9YbH99&z2YFuywe**_3KY>bLcmaVHsi1u}po^>&O&NQ;y8aczA{A5+c7ak1s6+y{ zO>l%br0#~~EpXYyrVnbKg6u@`Ej+}*r3bk7R5mq+)u5L_NvadtkO8$DnHfMn1J%`_ z{LIe~vKf+Ec|mJRKpUw*r9CTnc!3FA(;-qW4Dq5I10Sd{#mfWQ2@g7Jo97ECL4lGIC{9soad--5 zV+W0#Ft9R1S}$B+7l2o3ftnHE<`^R=ZwWGjT>)yC{VVQd5(lR;#u9J?4b-XumE+*H z9W=$sL24vWfwY+eRLDU}BeYhiEE72OGP(a=1dh4Cp!Nc&L;{tl;5Ira4Ot+{3rNU< zPRE3VEVxc!)(6!I-~tsIir_oqL2X@7Dgj|o&G4p^u@_vd{@%;t3~KZ;Lt3NIJjl;r z1x=~wke@(#g0UCssXwrs56zWee}P)RwvaFaMG_aNTfcz^ zVi72|z+*0gc9EEY2O1jz&6Gl0+mONr8jOE;!U_U#NP?0oBs4)O1spT{4AIc?57Gz% z74vKtK!qXr^1+Ru_4Ztl!jBbR9H6v3;dKDGZ4chsy zyYup8a4VF9;g1{ht{)7LUJnZ-XF|hYkU^ZG0vZGSc*}|n{GibxP#Z)H5@5Vo$_?-$ zDNq0+l^dYc!2_OT;|I0KHb4fNgu%HTlu97wi7Kc)0SQ)cvEtUr*#EDv6P!T*l)nb2 z{MX2ZIphC#|KBlhW8T5Q%pi-@s)UV?fO8%{sE5tT%)ITN>_2J7r;L}uC;Bj6?ebA*7=8TLiVSoPo+ZM;j#I%HI)?ZV`f7bsBnTi>h{(l1R9$dx1 z!NAWD43AGxEJ8+;csap)Rrz3J3gE$Xm~+6f2^w1BWCERR!vyZaae@!1WER-S3m%XG z4Z8>`n{qOP)@m|udHqk|IpagdgntYE&AZ7cz<50S-x9`{Y{tj`7#NfO&HHzh(Hg~7 zJg{&87v(UE!6OaeZ~))eu58N4$SSBP2si%Wza{^^Fvh@)mX&AZ`S*o^nStT|O%@U6 zMg|@R0R|BUgUt+_9H9O&v;JltMo=@I12k>}$rhkQ!4FQ3f)_wTETFhmg^k~UN>tET zC9|@rF^kAQ@qPZF0UZB*jFr0?wg1Nd>t!t7xog)`*!T_Of!y5OzbW|)ET9n_771b`pnnX26~nwTlE-X5e}i6!eme ze4r!5nK;44E@(V&1Czi;ZpJU5^TCBKY>)({aM0D_f{KjF>Y|`_w4xxRvZ%7DvMG37 z5sSzl6DD)UsdIlYs{U*IH<5{9Q_kkiIh$BT{^kDq_2(Va4#xW5CmFRjZp_=XiGhj1 z>;GkzIOdBC91J`RN}D-3xga43n^pj4YLLUgVF{Wj0uAVjGb%DF3mUV;{k_Oo>GzL; ziNWt*JCpvOsJuMJS^wJom>3xU`TcAC`-6EAC>?YCzsXV!9#xar%*-hYvVm8BGb1At zsISM!4{EImD~c+DHfAWQn=>-1n=68D%;92E{paOcxgdtIbiW^?%fCH-`~P*9o|+cI zT$>-Wpu%mR-@o1e_W2x$m~pazftkVU|4o({=0@;%H7GR*a`J#|;?&;^9-iLJ&d9=| z4O&sa4X(?0!Kr~CJhm?g>YIamR;I?RAnQTBA3;TRbCwv;2=a~{|JMB5?YHlrIHUGn z#_D~3%p#!pw**HD7+6>`SX~%Y z7(5t47!Ga-)!z`Nzadlrw4Po-e?yqS2E{uY)j{L8L|nEFT4Mq}rvbG4!jcPgw1%aa zrJALgrJH4#WtwG~Wt-(R%Vn0^ERR`&rduqZS~A6gG`U~c;I7ZGAqY~g>g!8y)Q4qa zQ+?@;rl93Au1q{E+MxUfE^;=Q3T%{R0w2^TqQAjaV1w<24YvBAI}brONT{nRD?w;e z6LT|TBXQ6cCoqFeR76~iT@55`W^N8r#tu;o5;ryis}Tdy%p&ZdgJs!;RK>(tGF1GM z<88X>Pl;l$7kn%SV?McS`VbNy@ZVcSi+;IhLJH zk7D8DQr8qwloiT}uGze8k+8D7XsINNVOUUvBnt}*BbN;)A9Odx9#(w@U54YE#d%af zX0=`4l321P>_R$Y0o?0LmyLOzfh@ zqKblc%%Jgj*j__%K}AzzQ#B^Yl^}9V%zN@`FaKNO9AzP<9@ZKc)f%QLW)|W6Z^`A# zJp1Zx<>lLI?U|lV{P!WYsFYDzD!sbhK4?W}SXk$ZAp7>}bg6%LON+u7g{Kx?U6hox z2)vJifvJEYk%65df&n~_&A`CG1!`V0En@(W@Gw|F;St7irh4Wu<~-)j%s*LNSt?m3 zvMgrV!E%L=H^^Tj1TbX+<&mNxlym@@$eC2%W z`0n$!^Pdx767Udc7g#0mThK}Ht&o?ng78FaZ6`cU<)>JK$$H9j>-H5D~|wU=sN)c&h;sf(&B zsB5d6sXMCssE4a3spqO!s5h(ksn1kjqP|}Jl7^?oX-!`(9xXGi71}jAraHfMBXx6i zXXxJ5)6?hHZ_wYSe_j8lL9D@f!z{zKh98X5jFuQj8h)m+Bh-h7Yw z4+}X97mG}bg%%4f)>`beIBFSgC2pl*Wo0$T>W8(ib&T}{>mAmItG8__O&7`OEoF3J?yE56}uQ3vdkZ4TuQX7ibme z7PunFHpnxmCun-mqM(OCZ-agYvjp=8O9!h58wcA5HwHfq{uulxggrzkL@q=##5ANQ zlrK~&R5jEv)F#v;G&nRqG%K_;v>~)7bb9FG&~>4^LXU>khpUAfh1-UEga?Pmhi8Xh zjfjp&k0^?$i|CA)8nGZ^O~m$y!x85qqa$xbK8buE`8$d&N+?P;N;AqN$|1@-YH!rZ zs4GzqqFzURi)M`Gi58F6icXBqjjoJtjh+}iH+p6C*60J#pJR$*rpN4!`4Vd#+Z1OW zmmRk%?s~jNe181pgt~-niNc9ti7S(sl6sSVB?l$PC1)j*cY{sgLmzlPi$1;Crt<8?e{+Tl|*Ce+k_fOuOyxaMD`H%D8<^L>TDc~=V zDo`u9P;jTPy2!dHzo@$CU~yRSr4rAQ(2~TGMI~2D%}TeH-YU~CODnrjKC}E``P&Me ziYXOOD|ITbSMgWnREt(;RPU~FtJz%hxz?q2cI~@5zq(!ZBK6xEVj3-pK6 z)yLFV+*jAn-!IkQHNjy**MzAPZcmJxcym(pq_jzelQvCyHd$)2+GL~2c9Xp(hfPkJ zoHx00a_i&?ljlrcK6&%x{gY2mzBc*MR zr^`-XIsL&5!5K<3Y-U8wD4#K5#)=tdXMCC|I@5S&+RUbzTV}qVr7^FJ)$ToAIL zVZp(LN()04wl6%s@Y|wGiz`5LZ(w}%pXY*>cz&C&4ALB+r8*49zPerjF|HbN*8PwE zSISb&ZpXmDzzJ%VfW%qiKb6U|}!>ts;h+%fP_!{~K8U!~akJ zzxn@=L7!n4V=u$5|9}1;{eKi>9@G|)H{kgH9|&aF#juNE6~iu&X`nNNK^WvVFy6($ zz`(&^$RNV7i$NY78lcq%pFp<%KluOhf6#~!NI%H+Zy2<}hO_+t@&DcbH~(+{zshit zNuPm%L7ai*|C|3u8Tc4_K`Owmy8r*v|Jw`<|CcbBGcYiM>}UA@?*GI8Z@|7j4m!|? zfq`KcV=0p!0|QvMAt)rlJ_o7Z_5c3=`wUFr&;x}n$Pa!DyBM}HmNI-|_{6Y^=>x+m z#!`k|jOPDuGJ@=t{{IhTI|C>zyBU%|;Q|gVkhu*1k1{a)Kl=Y4XjK?82Ko7aHz*GN z|6wph#sA-c)FUt`#QvB6FK6&$0J&rr!#0L(;LrtyBE$cG;CKVM6%>0&_3{Q2!V-^PIfyOx)JQ%zfxWG1n;sDgoWMJ$5*UiAd68Asu|6>;D z&Q>J-j10^yaf}lfSQ!{tXR#VHFfgh@XyyeB)r=Qe8JXDFIGLH47+6^rFfgqzU|4wy zraL#MxR}9#f#Ek3#16J@#!>|ahWQ|Q@O>@}3<@AN10w??2r%$4{NVV{z`?-&{~rS< z1IPb=3|tJH|Nk*?GjRU@%fQ3H_5UveF9Y}gzYKf~JpcbP@H6oK|H~l2!1w<`hAi*H=|2KmqgXsU? z3{nhY|9>+`Gl>8H%^<@d{{I()EQ7@VUkq{#lK+1($TLX&|HYudApQRrgCc|U|DOy> z3^M!OGpPLk!JxyS^8Y)7E`#d-?+khjYX848=rgGQ|IT2*pz;4ZgCT?F|L+V&44VJH zF&Hyw{r|>b!l3>C8-po>&i`)=W(>OjzcH9I=>Gr8V8NjG|0{zfgZ}@o3|0&V|GzR= zGZ_B=%3#A_^#3b^ErZejFAR1J#{a)C*fW^?|H9zFVEX?HgCm34|1S(q3}*j7GdMGt z|NqS3!eH_LGlMIG<^RtNZVXocKQp*9SpWab;K5-1{}Y2JgU$a>3|Mps5W(Q_|2;z_gXjPE3{ebT|KBr2GkE`h&k)1l z^Zz|VEQ9a=_Y83izW?7b#54H)f5(u(;Q#*}Ln1@K|91>Y41xdOF(fkt{(sAm!VvWT zEki0p@c*|AX$&F%-!h~#g#Lfakiii4|1Co%L)iZ}3|S1}|KBiVGerDjKIP{EM={}n?eL-PNZ3{?y%|6ejxGo=21$xy?P_WvbAEkpYM zmkf0b8UJ50)H7uKf5Fhekoo@wLnA}h{}&8R4B7u*Ff=pd{C~mF!jSX-IYTQ$?*Hcu zZ47z;pEI;Gnc*A5ssEQ5 zzB8Quf0^M2!|DH*7=ALG`G1Mw7sJ{Aml%FCocn)?;Sa<4|Cbp4GMxW^k>MZ1h5r{B z{xe+se-YF+W?*0{;bI*Hqai@g5MW^7W*Y^gAwbU%U|`^39|fZ!K+h0hVBn>vb)$BT zh5$Rb-wqlLWM*Js5Mf{jtrlhwVPIiC$H2g_iJ^gkjlqb4fq|DniJ^|+KcfMoA!8t8 zFk?Am1!FzqB*y?>Xd1qNk?1&j=ghKxpxL5v}c6^xaPlNkRnNiZE_y20GS+`-(#+{e6_ zc_s5I<^#+JSr}RPSOQt%SSH9Z$#Kca$vMb{$tB2T$u-D*QV>>

@kj0K3@|!_9(7 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


Bo+tVFQ%jh>Oz9*Ul?r;TDNN?CN2!3A!jIq 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 literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/CourierPrime-Bold.ttf.import b/addons/gut/fonts/CourierPrime-Bold.ttf.import new file mode 100644 index 0000000..c498c6c --- /dev/null +++ b/addons/gut/fonts/CourierPrime-Bold.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://bhjgpy1dovmyq" +path="res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/CourierPrime-Bold.ttf" +dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/CourierPrime-BoldItalic.ttf b/addons/gut/fonts/CourierPrime-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0afaa98dffc35a90e900dfb15e142a7f71224c1b GIT binary patch literal 77384 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�#GVpWL@oO$tVp^ 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 E0EWs#;{X5v literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import new file mode 100644 index 0000000..30bf3fe --- /dev/null +++ b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://n6mxiov5sbgc" +path="res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/CourierPrime-BoldItalic.ttf" +dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/CourierPrime-Italic.ttf b/addons/gut/fonts/CourierPrime-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f8a20bd097284f65ea89f88f742f6d2278df9215 GIT binary patch literal 76496 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 z

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⁣Sny$diz_4nWald`ayo=cf&fY%zyWx@n zNGH!V%^&yg&*9f#ZI)zUI8dT%R5kN4=qv-!GPB`>4QBr5KhS + + + + + diff --git a/addons/gut/images/HSplitContainer.svg.import b/addons/gut/images/HSplitContainer.svg.import new file mode 100644 index 0000000..37a460a --- /dev/null +++ b/addons/gut/images/HSplitContainer.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ljc2viafngwd" +path="res://.godot/imported/HSplitContainer.svg-81e594d94ee80e4176e099672ef6025b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/HSplitContainer.svg" +dest_files=["res://.godot/imported/HSplitContainer.svg-81e594d94ee80e4176e099672ef6025b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/gut/images/Script.svg b/addons/gut/images/Script.svg new file mode 100644 index 0000000..b105e17 --- /dev/null +++ b/addons/gut/images/Script.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/gut/images/Script.svg.import b/addons/gut/images/Script.svg.import new file mode 100644 index 0000000..8dc4fbf --- /dev/null +++ b/addons/gut/images/Script.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cavojn74qp7ij" +path="res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/Script.svg" +dest_files=["res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/gut/images/VSplitContainer.svg b/addons/gut/images/VSplitContainer.svg new file mode 100644 index 0000000..84ecd62 --- /dev/null +++ b/addons/gut/images/VSplitContainer.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/addons/gut/images/VSplitContainer.svg.import b/addons/gut/images/VSplitContainer.svg.import new file mode 100644 index 0000000..0dd7f16 --- /dev/null +++ b/addons/gut/images/VSplitContainer.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bhew20crsywxr" +path="res://.godot/imported/VSplitContainer.svg-6cec29fefa7e65d6ff7c2fdb9a1bb9ab.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/VSplitContainer.svg" +dest_files=["res://.godot/imported/VSplitContainer.svg-6cec29fefa7e65d6ff7c2fdb9a1bb9ab.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/gut/images/eyey.png b/addons/gut/images/eyey.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d388ae28bae0480c66b636dc2e2e3e2443e182 GIT binary patch literal 3721 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 literal 0 HcmV?d00001 diff --git a/addons/gut/images/green.png.import b/addons/gut/images/green.png.import new file mode 100644 index 0000000..25c0770 --- /dev/null +++ b/addons/gut/images/green.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvrnfjkcmpr8s" +path="res://.godot/imported/green.png-e3a17091688e10a7013279b38edc7f8a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/green.png" +dest_files=["res://.godot/imported/green.png-e3a17091688e10a7013279b38edc7f8a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gut/images/red.png b/addons/gut/images/red.png new file mode 100644 index 0000000000000000000000000000000000000000..3d9dec14b3dd89ec36dbb5b7008c36a10afe8e4b GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe7G?$phWh0Ub_@&*iUB?$t_%zeznPfupv5*S z#taM$TqQw%!3^v|NvVCSuipP1S0}oQfq@~?)5S5Q;#P7(LW0TbITLjbFfhGa-LQb= zNKh;*o1;UTNXyPCjg6_IA{GW6hiA0%gc&?bYSWP@mUI^H+1ZpR*0WP+W@kXd8s0>S oO`;s#g6AAdRSv!6;AdnAzm^_);N#jW3=9kmp00i_>zopr03ZND?f?J) literal 0 HcmV?d00001 diff --git a/addons/gut/images/red.png.import b/addons/gut/images/red.png.import new file mode 100644 index 0000000..3edaaa6 --- /dev/null +++ b/addons/gut/images/red.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ba2sgost7my3x" +path="res://.godot/imported/red.png-47a557c3922e800f76686bc1a4ad0c3c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/red.png" +dest_files=["res://.godot/imported/red.png-47a557c3922e800f76686bc1a4ad0c3c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gut/images/yellow.png b/addons/gut/images/yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7dc35b0fc95b004901c205612515c2c3621942 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe7G?$phWh0Ub_@&*iUB?$t_%ze|F7Xgr>7-P zVqjq4DhcunW?&aeO6^;H_5Sa;I?-JW3=EN;E{-7;w~`YQ5=>stnW%Gsf$81qh6OA~ zf?`?O939d`T6RupY)lmuu`uX3JfoE-%-~s4n~p@Wq_cR>&Zb1Mo}EH7I|CZl@Fq%Z o66NR?Jm*-da_A)oKO;l InputEventMouseButton: + var event = InputEventMouseButton.new() + event.position = position + if(global_position != null): + event.global_position = global_position + event.pressed = pressed + event.button_index = button_index + + return event + + +## Returns an [InputEventKey] event with [code]pressed = false[/code]. [param which] can be a character or a [code]KEY_*[/code] constant. +static func key_up(which) -> InputEventKey: + var event = InputEventKey.new() + event.keycode = _to_scancode(which) + event.pressed = false + return event + + +## Returns an [InputEventKey] event with [code]pressed = true[/code]. [param which] can be a character or a [code]KEY_*[/code] constant. +static func key_down(which) -> InputEventKey: + var event = InputEventKey.new() + event.keycode = _to_scancode(which) + event.pressed = true + return event + + +## Returns an "action up" [InputEventAction] instance. [param which] is the name of the action defined in the Key Map. +static func action_up(which, strength=1.0) -> InputEventAction: + var event = InputEventAction.new() + event.action = which + event.strength = strength + return event + + +## Returns an "action down" [InputEventAction] instance. [param which] is the name of the action defined in the Key Map. +static func action_down(which, strength=1.0) -> InputEventAction: + var event = InputEventAction.new() + event.action = which + event.strength = strength + event.pressed = true + return event + + +## Returns a "button down" [InputEventMouseButton] for the left mouse button. +static func mouse_left_button_down(position, global_position=null) -> InputEventMouseButton: + var event = new_mouse_button_event(position, global_position, true, MOUSE_BUTTON_LEFT) + return event + + +## Returns a "button up" [InputEventMouseButton] for the left mouse button. +static func mouse_left_button_up(position, global_position=null) -> InputEventMouseButton: + var event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_LEFT) + return event + + +## Returns a "double click" [InputEventMouseButton] for the left mouse button. +static func mouse_double_click(position, global_position=null) -> InputEventMouseButton: + var event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_LEFT) + event.double_click = true + return event + + +## Returns a "button down" [InputEventMouseButton] for the right mouse button. +static func mouse_right_button_down(position, global_position=null) -> InputEventMouseButton: + var event = new_mouse_button_event(position, global_position, true, MOUSE_BUTTON_RIGHT) + return event + + +## Returns a "button up" [InputEventMouseButton] for the right mouse button. +static func mouse_right_button_up(position, global_position=null) -> InputEventMouseButton: + var event = new_mouse_button_event(position, global_position, false, MOUSE_BUTTON_RIGHT) + return event + + +## Returns a [InputEventMouseMotion] to move the mouse the specified positions. +static func mouse_motion(position, global_position=null) -> InputEventMouseMotion: + var event = InputEventMouseMotion.new() + event.position = position + if(global_position != null): + event.global_position = global_position + return event + + +## Returns an [InputEventMouseMotion] that moves the mouse [param offset] +## from the last [method mouse_motion] or [method mouse_motion] call. +static func mouse_relative_motion(offset, last_motion_event=null, speed=Vector2(0, 0)) -> InputEventMouseMotion: + var event = null + if(last_motion_event == null): + event = mouse_motion(offset) + event.velocity = speed + else: + event = last_motion_event.duplicate() + event.position += offset + event.global_position += offset + event.relative = offset + event.velocity = speed + return event + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# Description +# ----------- +# ############################################################################## diff --git a/addons/gut/input_factory.gd.uid b/addons/gut/input_factory.gd.uid new file mode 100644 index 0000000..df4fdec --- /dev/null +++ b/addons/gut/input_factory.gd.uid @@ -0,0 +1 @@ +uid://b4s8333o8s4cp diff --git a/addons/gut/input_sender.gd b/addons/gut/input_sender.gd new file mode 100644 index 0000000..5ea623d --- /dev/null +++ b/addons/gut/input_sender.gd @@ -0,0 +1,651 @@ +class_name GutInputSender +## The GutInputSender class. It sends input to places. +## +## [br][br] +## GUT Wiki: [url=https://gut.readthedocs.io]https://gut.readthedocs.io[/url][br] +## See [wiki]Mocking-Input[/wiki] for examples. +## [br][br] +## This class can be used to send [code]InputEvent*[/code] events to various +## objects. It also allows you to script out a series of inputs and play +## them back in real time. You could use it to:[br] +## - Verify that jump height depends on how long the jump button is pressed.[br] +## - Double tap a direction performs a dash.[br] +## - Down, Down-Forward, Forward + punch throws a fireball.[br] +## [br][br] +## And much much more. +## [br][br] +## As of 9.3.1 you can use [code skip-lint]GutInputSender[/code] instead of [code]InputSender[/code]. It's the same thing, but [code skip-lint]GutInputSender[/code] is a [code]class_name[/code] so you may have less warnings and auto-complete will work. +## [br][br] +## [b]Warning[/b][br] +## If you move the Godot window to a different monitor while tests are running it can cause input tests to fail. [url=https://github.com/bitwes/Gut/issues/643]This issue[/url] has more details. + + + +# Implemented InputEvent* convenience methods +# InputEventAction +# InputEventKey +# InputEventMouseButton +# InputEventMouseMotion + +# Yet to implement InputEvents +# InputEventJoypadButton +# InputEventJoypadMotion +# InputEventMagnifyGesture +# InputEventMIDI +# InputEventPanGesture +# InputEventScreenDrag +# InputEventScreenTouch + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class InputSenderQueueItem: + extends Node + + var events = [] + var time_delay = null + var frame_delay = null + var _waited_frames = 0 + var _is_ready = false + var _delay_started = false + + signal event_ready + + # TODO should this be done in _physics_process instead or should it be + # configurable? + func _physics_process(delta): + if(frame_delay > 0 and _delay_started): + _waited_frames += 1 + if(_waited_frames >= frame_delay): + event_ready.emit() + + func _init(t_delay,f_delay): + time_delay = t_delay + frame_delay = f_delay + _is_ready = time_delay == 0 and frame_delay == 0 + + func _on_time_timeout(): + _is_ready = true + event_ready.emit() + + func _delay_timer(t): + return Engine.get_main_loop().root.get_tree().create_timer(t) + + func is_ready(): + return _is_ready + + func start(): + _delay_started = true + if(time_delay > 0): + var t = _delay_timer(time_delay) + t.connect("timeout",Callable(self,"_on_time_timeout")) + + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class InputSenderMouseDraw: + extends Node2D + + var down_color = Color(1, 1, 1, .25) + var up_color = Color(0, 0, 0, .25) + var line_color = Color(1, 0, 0) + var disabled = true : + get : return disabled + set(val) : + disabled = val + queue_redraw() + + var _draw_at = Vector2(0, 0) + var _b1_down = false + var _b2_down = false + + + func draw_event(event): + if(event is InputEventMouse): + _draw_at = event.position + if(event is InputEventMouseButton): + if(event.button_index == MOUSE_BUTTON_LEFT): + _b1_down = event.pressed + elif(event.button_index == MOUSE_BUTTON_RIGHT): + _b2_down = event.pressed + queue_redraw() + + + func _draw_cicled_cursor(): + var r = 10 + var b1_color = up_color + var b2_color = up_color + + if(_b1_down): + var pos = _draw_at - (Vector2(r * 1.5, 0)) + draw_arc(pos, r / 2, 0, 360, 180, b1_color) + + if(_b2_down): + var pos = _draw_at + (Vector2(r * 1.5, 0)) + draw_arc(pos, r / 2, 0, 360, 180, b2_color) + + draw_arc(_draw_at, r, 0, 360, 360, line_color, 1) + draw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color) + draw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color) + + + func _draw_square_cursor(): + var r = 10 + var b1_color = up_color + var b2_color = up_color + + if(_b1_down): + b1_color = down_color + + if(_b2_down): + b2_color = down_color + + var blen = r * .75 + # left button rectangle + draw_rect(Rect2(_draw_at - Vector2(blen, blen), Vector2(blen, blen * 2)), b1_color) + # right button rectrangle + draw_rect(Rect2(_draw_at - Vector2(0, blen), Vector2(blen, blen * 2)), b2_color) + # Crosshair + draw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color) + draw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color) + + + func _draw(): + if(disabled): + return + _draw_square_cursor() + + + + + + + +# ############################################################################## +# +# ############################################################################## +## Local reference to the GutInputFactory static class +const 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.' + +var _lgr = GutUtils.get_logger() +var _receivers = [] +var _input_queue = [] +var _next_queue_item = null + +# used by hold_for and echo. +var _last_event = null +# indexed by keycode, each entry contains a boolean value indicating the +# last emitted "pressed" value for that keycode. +var _pressed_keys = {} +var _pressed_actions = {} +var _pressed_mouse_buttons = {} + +var _auto_flush_input = false +var _tree_items_parent = null +var _mouse_draw = null; + +var _default_mouse_position = { + position = Vector2(0, 0), + global_position = Vector2(0, 0) +} + +var _last_mouse_position = { +} + +## Warp mouse when sending InputEventMouse* events +var mouse_warp = false +## Draw mouse position cross hairs. Useful to see where the mouse is at +## when not using [member mouse_warp] +var draw_mouse = true + +## Emitted when all events in the input queue have been sent. +signal idle + + +## Accepts a single optional receiver. +func _init(r=null): + if(r != null): + add_receiver(r) + + _last_mouse_position = _default_mouse_position.duplicate() + _tree_items_parent = Node.new() + Engine.get_main_loop().root.add_child(_tree_items_parent) + + _mouse_draw = InputSenderMouseDraw.new() + _tree_items_parent.add_child(_mouse_draw) + _mouse_draw.disabled = false + + +func _notification(what): + if(what == NOTIFICATION_PREDELETE): + if(is_instance_valid(_tree_items_parent)): + _tree_items_parent.queue_free() + + +func _add_queue_item(item): + item.connect("event_ready", _on_queue_item_ready.bind(item)) + _next_queue_item = item + _input_queue.append(item) + _tree_items_parent.add_child(item) + if(_input_queue.size() == 1): + item.start() + + +func _handle_pressed_keys(event): + if(event is InputEventKey): + if((event.pressed and !event.echo) and is_key_pressed(event.keycode)): + _lgr.warn(str("InputSender: key_down called for ", event.as_text(), " when that key is already pressed. ", INPUT_WARN)) + _pressed_keys[event.keycode] = event.pressed + elif(event is InputEventAction): + if(event.pressed and is_action_pressed(event.action)): + _lgr.warn(str("InputSender: action_down called for ", event.action, " when that action is already pressed. ", INPUT_WARN)) + _pressed_actions[event.action] = event.pressed + elif(event is InputEventMouseButton): + if(event.pressed and is_mouse_button_pressed(event.button_index)): + _lgr.warn(str("InputSender: mouse_button_down called for ", event.button_index, " when that mouse button is already pressed. ", INPUT_WARN)) + _pressed_mouse_buttons[event.button_index] = event + + +func _handle_mouse_position(event): + if(event is InputEventMouse): + _mouse_draw.disabled = !draw_mouse + _mouse_draw.draw_event(event) + if(mouse_warp): + DisplayServer.warp_mouse(event.position) + + +func _send_event(event): + _handle_mouse_position(event) + _handle_pressed_keys(event) + + for r in _receivers: + if(r == Input): + Input.parse_input_event(event) + if(event is InputEventAction): + if(event.pressed): + Input.action_press(event.action) + else: + Input.action_release(event.action) + if(_auto_flush_input): + Input.flush_buffered_events() + else: + if(r.has_method(&"_input")): + r._input(event) + + if(r.has_signal(&"gui_input")): + r.gui_input.emit(event) + + if(r.has_method(&"_gui_input")): + r._gui_input(event) + + if(r.has_method(&"_unhandled_input")): + r._unhandled_input(event) + + +func _send_or_record_event(event): + _last_event = event + if(_next_queue_item != null): + _next_queue_item.events.append(event) + else: + _send_event(event) + + +func _set_last_mouse_positions(event : InputEventMouse): + _last_mouse_position.position = event.position + _last_mouse_position.global_position = event.global_position + + +func _apply_last_position_and_set_last_position(event, position, global_position): + event.position = GutUtils.nvl(position, _last_mouse_position.position) + event.global_position = GutUtils.nvl( + global_position, _last_mouse_position.global_position) + _set_last_mouse_positions(event) + + +func _new_defaulted_mouse_button_event(position, global_position): + var event = InputEventMouseButton.new() + _apply_last_position_and_set_last_position(event, position, global_position) + return event + + +func _new_defaulted_mouse_motion_event(position, global_position): + var event = InputEventMouseMotion.new() + _apply_last_position_and_set_last_position(event, position, global_position) + for key in _pressed_mouse_buttons: + if(_pressed_mouse_buttons[key].pressed): + event.button_mask += key + return event + + +# ------------------------------ +# Events +# ------------------------------ +func _on_queue_item_ready(item): + for event in item.events: + _send_event(event) + + var done_event = _input_queue.pop_front() + done_event.queue_free() + + if(_input_queue.size() == 0): + _next_queue_item = null + idle.emit() + else: + _input_queue[0].start() + + +# ------------------------------ +# Public +# ------------------------------ + + +## Add an object to receive input events. +func add_receiver(obj): + _receivers.append(obj) + + +## Returns the receivers that have been added. +func get_receivers(): + return _receivers + + +## Returns true if the input queue has items to be processed, false if not. +func is_idle(): + return _input_queue.size() == 0 + +func is_key_pressed(which): + var event = GutInputFactory.key_up(which) + return _pressed_keys.has(event.keycode) and _pressed_keys[event.keycode] + +func is_action_pressed(which): + return _pressed_actions.has(which) and _pressed_actions[which] + +func is_mouse_button_pressed(which): + return _pressed_mouse_buttons.has(which) and _pressed_mouse_buttons[which].pressed + + +## Get the value of [method set_auto_flush_input]. +func get_auto_flush_input(): + return _auto_flush_input + + +## Enable/Disable auto flushing of input. When enabled the [GutInputSender] +## will call [code]Input.flush_buffered_events[/code] after each event is sent. +## See the "use_accumulated_input" section in [wiki]Mocking-Input[/wiki] for more +## information. +func set_auto_flush_input(val): + _auto_flush_input = val + + +## Adds a delay between the last input queue item added and any queue item added +## next. By default this will wait [param t] seconds. You can specify a +## number of frames to wait by passing a string composed of a number and "f". +## For example [code]wait("5f")[/code] will wait 5 frames. +func wait(t): + if(typeof(t) == TYPE_STRING): + var suffix = t.substr(t.length() -1, 1) + var val = t.rstrip('s').rstrip('f').to_float() + + if(suffix.to_lower() == 's'): + wait_secs(val) + elif(suffix.to_lower() == 'f'): + wait_frames(val) + else: + wait_secs(t) + + return self + + +## Clears the input queue and any state such as the last event sent and any +## pressed actions/buttons. Does not clear the list of receivers. +## [br][br] +## This should be done between each test when the [GutInputSender] is a class +## level variable so that state does not leak between tests. +func clear(): + _last_event = null + _next_queue_item = null + + for item in _input_queue: + item.free() + _input_queue.clear() + + _pressed_keys.clear() + _pressed_actions.clear() + _pressed_mouse_buttons.clear() + _last_mouse_position = _default_mouse_position.duplicate() + + +# ------------------------------ +# Event methods +# ------------------------------ + +## Sends a [InputEventKey] event with [code]pressed = false[/code]. [param which] can be a character or a [code]KEY_*[/code] constant. +func key_up(which): + var event = GutInputFactory.key_up(which) + _send_or_record_event(event) + return self + + +## Sends a [InputEventKey] event with [code]pressed = true[/code]. [param which] can be a character or a [code]KEY_*[/code] constant. +func key_down(which): + var event = GutInputFactory.key_down(which) + _send_or_record_event(event) + return self + + +## Sends an echo [InputEventKey] event of the last key event. +func key_echo(): + if(_last_event != null and _last_event is InputEventKey): + var new_key = _last_event.duplicate() + new_key.echo = true + _send_or_record_event(new_key) + return self + + +## Sends a "action up" [InputEventAction] instance. [param which] is the name of the action defined in the Key Map. +func action_up(which, strength=1.0): + var event = GutInputFactory.action_up(which, strength) + _send_or_record_event(event) + return self + + +## Sends a "action down" [InputEventAction] instance. [param which] is the name of the action defined in the Key Map. +func action_down(which, strength=1.0): + var event = GutInputFactory.action_down(which, strength) + _send_or_record_event(event) + return self + + +## Sends a "button down" [InputEventMouseButton] for the left mouse button. +func mouse_left_button_down(position=null, global_position=null): + var event = _new_defaulted_mouse_button_event(position, global_position) + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + _send_or_record_event(event) + return self + + +## Sends a "button up" [InputEventMouseButton] for the left mouse button. +func mouse_left_button_up(position=null, global_position=null): + var event = _new_defaulted_mouse_button_event(position, global_position) + event.pressed = false + event.button_index = MOUSE_BUTTON_LEFT + _send_or_record_event(event) + return self + + +## Sends a "double click" [InputEventMouseButton] for the left mouse button. +func mouse_double_click(position=null, global_position=null): + var event = GutInputFactory.mouse_double_click(position, global_position) + event.double_click = true + _send_or_record_event(event) + return self + + +## Sends a "button down" [InputEventMouseButton] for the right mouse button. +func mouse_right_button_down(position=null, global_position=null): + var event = _new_defaulted_mouse_button_event(position, global_position) + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + _send_or_record_event(event) + return self + + +## Sends a "button up" [InputEventMouseButton] for the right mouse button. +func mouse_right_button_up(position=null, global_position=null): + var event = _new_defaulted_mouse_button_event(position, global_position) + event.pressed = false + event.button_index = MOUSE_BUTTON_RIGHT + _send_or_record_event(event) + return self + + +## Sends a [InputEventMouseMotion] to move the mouse the specified positions. +func mouse_motion(position, global_position=null): + var event = _new_defaulted_mouse_motion_event(position, global_position) + _send_or_record_event(event) + return self + + +## Sends a [InputEventMouseMotion] that moves the mouse [param offset] +## from the last [method mouse_motion] or [method mouse_set_position] call. +func mouse_relative_motion(offset, speed=Vector2(0, 0)): + var last_event = _new_defaulted_mouse_motion_event(null, null) + var event = GutInputFactory.mouse_relative_motion(offset, last_event, speed) + _set_last_mouse_positions(event) + _send_or_record_event(event) + return self + + +## Sets the mouse's position. This does not send an event. This position will +## be used for the next call to [method mouse_relative_motion]. +func mouse_set_position(position, global_position=null): + var event = _new_defaulted_mouse_motion_event(position, global_position) + return self + + +## Performs a left click at the given position. +func mouse_left_click_at(where, duration = '5f'): + wait_frames(1) + mouse_left_button_down(where) + hold_for(duration) + wait_frames(10) + return self + + +## Create your own event and use this to send it to all receivers. +func send_event(event): + _send_or_record_event(event) + return self + + +## Releases all [InputEventKey], [InputEventAction], and [InputEventMouseButton] +## events that have passed through this instance. These events could have been +## generated via the various [code]_down[/code] methods or passed to +## [method send_event]. +## [br][br] +## This will send the "release" event ([code]pressed = false[/code]) to all +## receivers. This should be done between each test when using `Input` as a +## receiver. +func release_all(): + for key in _pressed_keys: + if(_pressed_keys[key]): + _send_event(GutInputFactory.key_up(key)) + _pressed_keys.clear() + + for key in _pressed_actions: + if(_pressed_actions[key]): + _send_event(GutInputFactory.action_up(key)) + _pressed_actions.clear() + + for key in _pressed_mouse_buttons: + var event = _pressed_mouse_buttons[key].duplicate() + if(event.pressed): + event.pressed = false + _send_event(event) + _pressed_mouse_buttons.clear() + + return self + +## Same as [method wait] but only accepts a number of frames to wait. +func wait_frames(num_frames): + var item = InputSenderQueueItem.new(0, num_frames) + _add_queue_item(item) + return self + + +## Same as [method wait] but only accepts a number of seconds to wait. +func wait_secs(num_secs): + var item = InputSenderQueueItem.new(num_secs, 0) + _add_queue_item(item) + return self + + +## This is a special [method wait] that will emit the previous input queue item +## with [code]pressed = false[/code] after a delay. If you pass a number then +## it will wait that many seconds. You can also use the `"4f"` format to wait +## a specific number of frames. +## [br][br] +## For example [code]sender.action_down('jump').hold_for("10f")[/code] will +## cause two [InputEventAction] instances to be sent. The "jump-down" event +## from [method action_down] and then a "jump-up" event after 10 frames. +func hold_for(duration): + if(_last_event != null and _last_event.pressed): + var next_event = _last_event.duplicate() + next_event.pressed = false + + wait(duration) + send_event(next_event) + return self + + +## Same as [method hold_for] but specifically holds for a number of physics +## frames. +func hold_frames(duration:int): + return hold_for(str(duration, 'f')) + + +## Same as [method hold_for] but specifically holds for a number of seconds. +func hold_seconds(duration:float): + return hold_for(duration) + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# Description +# ----------- +# This class sends input to one or more recievers. The receivers' _input, +# _unhandled_input, and _gui_input are called sending InputEvent* events. +# InputEvents can be sent via the helper methods or a custom made InputEvent +# can be sent via send_event(...) +# +# ############################################################################## diff --git a/addons/gut/input_sender.gd.uid b/addons/gut/input_sender.gd.uid new file mode 100644 index 0000000..1ff32fc --- /dev/null +++ b/addons/gut/input_sender.gd.uid @@ -0,0 +1 @@ +uid://da3cy6yko53dk diff --git a/addons/gut/junit_xml_export.gd b/addons/gut/junit_xml_export.gd new file mode 100644 index 0000000..466e437 --- /dev/null +++ b/addons/gut/junit_xml_export.gd @@ -0,0 +1,111 @@ +## Exposes functionality to export results of a test run in JUnit XML format. +## +## This class exposes two methods for exporting GUT test results to XML. +## One returns a string representing the XML, and the other writes it to +## a file. + +var _strutils := GutStringUtils.new() +var _exporter = GutUtils.ResultExporter.new() + + +func _wrap_cdata(content) -> String: + return "" + + +func _add_attr(name, value) -> String: + return str(name, '="', value, '" ') + + +func _export_test_result(test: Dictionary) -> String: + var to_return := '' + + # Right now the pending and failure messages won't fit in the message + # attribute because they can span multiple lines and need to be escaped. + if(test.status == 'pending'): + var skip_tag := str("", _wrap_cdata(test.pending[0]), "") + to_return += skip_tag + elif(test.status == 'fail'): + var fail_tag := str("", _wrap_cdata(test.failing[0]), "") + to_return += fail_tag + + return to_return + + +func _export_tests(script_result: Dictionary, classname: String) -> String: + var to_return := "" + + for test_name: String in script_result.keys(): + var test: Dictionary = script_result[test_name] + var assert_count = test.passing.size() + test.failing.size() + to_return += " float: + var to_return := 0.0 + + for test: Dictionary in script_result.values(): + to_return += test.time_taken + + return to_return + + +func _export_scripts(exp_results: Dictionary) -> String: + var to_return := "" + for script_path: String in exp_results.test_scripts.scripts.keys(): + var s: Dictionary = exp_results.test_scripts.scripts[script_path] + to_return += " String: + var exp_results: Dictionary = _exporter.get_results_dictionary(gut) + var to_return := '' + "\n" + to_return += ' int: + var xml := get_results_xml(gut) + + var f_result: int = GutUtils.write_file(path, xml) + if(f_result != OK): + var msg := str("Error: ", f_result, ". Could not create export file ", path) + GutUtils.get_logger().error(msg) + + return f_result diff --git a/addons/gut/junit_xml_export.gd.uid b/addons/gut/junit_xml_export.gd.uid new file mode 100644 index 0000000..ccf518e --- /dev/null +++ b/addons/gut/junit_xml_export.gd.uid @@ -0,0 +1 @@ +uid://23m7uwtgs8nj diff --git a/addons/gut/lazy_loader.gd b/addons/gut/lazy_loader.gd new file mode 100644 index 0000000..9d42fa0 --- /dev/null +++ b/addons/gut/lazy_loader.gd @@ -0,0 +1,39 @@ +@tool +# ------------------------------------------------------------------------------ +# Static +# ------------------------------------------------------------------------------ +static var usage_counter = load('res://addons/gut/thing_counter.gd').new() +static var WarningsManager = load('res://addons/gut/warnings_manager.gd') + +static func load_all(): + for key in usage_counter.things: + key.get_loaded() + + +static func print_usage(): + for key in usage_counter.things: + print(key._path, ' (', usage_counter.things[key], ')') + + +static func clear(): + usage_counter.things.clear() + +# ------------------------------------------------------------------------------ +# Class +# ------------------------------------------------------------------------------ +var _loaded = null +var _path = null + +func _init(path): + _path = path + usage_counter.add_thing_to_count(self) + + +func get_loaded(): + if(_loaded == null): + _loaded = WarningsManager.load_script_ignoring_all_warnings(_path) + usage_counter.add(self) + return _loaded + + + diff --git a/addons/gut/lazy_loader.gd.uid b/addons/gut/lazy_loader.gd.uid new file mode 100644 index 0000000..0ce4b79 --- /dev/null +++ b/addons/gut/lazy_loader.gd.uid @@ -0,0 +1 @@ +uid://rcaa7rmoaaub diff --git a/addons/gut/logger.gd b/addons/gut/logger.gd new file mode 100644 index 0000000..d9ce723 --- /dev/null +++ b/addons/gut/logger.gd @@ -0,0 +1,431 @@ +var types = { + debug = 'debug', + deprecated = 'deprecated', + expected_error = 'expected_error', + error = 'error', + failed = 'failed', + info = 'info', + normal = 'normal', + orphan = 'orphan', + passed = 'passed', + pending = 'pending', + risky = 'risky', + warn = 'warn', +} + +var fmts = { + red = 'red', + yellow = 'yellow', + green = 'green', + blue = 'blue', + + bold = 'bold', + underline = 'underline', + + none = null +} + +var _type_data = { + types.debug: {disp='DEBUG', enabled=true, fmt=fmts.bold}, + types.deprecated: {disp='DEPRECATED', enabled=true, fmt=fmts.none}, + types.error: {disp='GUT ERROR', enabled=true, fmt=fmts.red}, + types.expected_error: {disp="ExpectedError", enabled=true, fmt=fmts.blue}, + types.failed: {disp='Failed', enabled=true, fmt=fmts.red}, + types.info: {disp='INFO', enabled=true, fmt=fmts.bold}, + types.normal: {disp='NORMAL', enabled=true, fmt=fmts.none}, + types.orphan: {disp='Orphans', enabled=true, fmt=fmts.yellow}, + types.passed: {disp='Passed', enabled=true, fmt=fmts.green}, + types.pending: {disp='Pending', enabled=true, fmt=fmts.yellow}, + types.risky: {disp='Risky', enabled=true, fmt=fmts.yellow}, + types.warn: {disp='GUT WARNING', enabled=true, fmt=fmts.yellow}, +} + +var _logs = { + types.warn: [], + types.error: [], + types.info: [], + types.debug: [], + types.deprecated: [], + types.expected_error: [], + types.failed: [], +} + +var _printers = { + terminal = null, + gui = null, + console = null +} + +var _gut = null +var _indent_level = 0 +var _min_indent_level = 0 +var _indent_string = ' ' +var _less_test_names = false +var _yield_calls = 0 +var _last_yield_text = '' + + + + +func _init(): + _printers.terminal = GutUtils.Printers.GutTerminalPrinter.new() + _printers.console = GutUtils.Printers.GutConsolePrinter.new() + # There were some problems in the timing of disabling this at the right + # time in gut_cmdln so it is disabled by default. This is enabled + # by plugin_control.gd based on settings. + _printers.console.set_disabled(true) + + +func _indent_text(text): + var to_return = text + var ending_newline = '' + + if(text.ends_with("\n")): + ending_newline = "\n" + to_return = to_return.left(to_return.length() -1) + + var pad = get_indent() + to_return = to_return.replace("\n", "\n" + pad) + to_return += ending_newline + + return pad + to_return + + +func _should_print_to_printer(key_name): + return _printers[key_name] != null and !_printers[key_name].get_disabled() + + +func _print_test_name(): + if(_gut == null): + return + + var cur_test = _gut.get_current_test_object() + if(cur_test == null): + return false + + if(!cur_test.has_printed_name): + var param_text = '' + if(cur_test.arg_count > 0): + # Just an FYI, parameter_handler in gut might not be set yet so can't + # use it here for cooler output. + param_text = '' + _output(str('* ', cur_test.name, param_text, "\n")) + cur_test.has_printed_name = true + + +func _output(text, fmt=null): + for key in _printers: + if(_should_print_to_printer(key)): + _printers[key].send(text, fmt) + + +func _log(text, fmt=fmts.none): + _print_test_name() + var indented = _indent_text(text) + _output(indented, fmt) + +# --------------- +# Get Methods +# --------------- +func get_warnings(): + return get_log_entries(types.warn) + + +func get_errors(): + return get_log_entries(types.error) + + +func get_infos(): + return get_log_entries(types.info) + + +func get_debugs(): + return get_log_entries(types.debug) + + +func get_deprecated(): + return get_log_entries(types.deprecated) + + +func get_count(log_type=null): + var count = 0 + if(log_type == null): + for key in _logs: + count += _logs[key].size() + else: + count = _logs[log_type].size() + return count + + +func get_log_entries(log_type): + return _logs[log_type] + + +func get_indent(): + var pad = '' + for i in range(_indent_level): + pad += _indent_string + + return pad + + +# --------------- +# Log methods +# --------------- +func _output_type(type, text): + var td = _type_data[type] + if(!td.enabled): + # if(_logs.has(type)): + # _logs[type].append(text) + return + + _print_test_name() + if(type != types.normal): + if(_logs.has(type)): + _logs[type].append(text) + + var start = str('[', td.disp, ']') + if(text != null and text != ''): + start += ': ' + else: + start += ' ' + var indented_start = _indent_text(start) + var indented_end = _indent_text(text) + indented_end = indented_end.lstrip(_indent_string) + _output(indented_start, td.fmt) + _output(indented_end + "\n") + + +func _output_type_no_indent(type, text): + var td = _type_data[type] + if(!td.enabled): + # if(_logs.has(type)): + # _logs[type].append(text) + return + + _print_test_name() + if(type != types.normal): + if(_logs.has(type)): + _logs[type].append(text) + + var start = str('[', td.disp, ']') + _output(start, td.fmt) + _output(text + "\n") + + +func debug(text): + _output_type(types.debug, text) + + +# supply some text or the name of the deprecated method and the replacement. +func deprecated(text, alt_method=null): + var msg = text + if(alt_method): + msg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.') + return _output_type(types.deprecated, msg) + + +func error(text): + _output_type(types.error, text) + # Use the _gut one instead of GutUtils.get_error_tracker() for testing + # purposes. This probably means this should have its own reference but + # that seems too difficult now. + if(_gut != null): + _gut.error_tracker.add_gut_error(text) + + +func expected_error(text): + _output_type_no_indent(types.expected_error, text) + + +func failed(text): + _output_type(types.failed, text) + + +func info(text): + _output_type(types.info, text) + + +func orphan(text): + var td = _type_data["orphan"] + if(!td.enabled): + return + _output(_indent_text(text), td.fmt) + _output("\n") + # _output_type(types.orphan, text) + + +func passed(text): + _output_type(types.passed, text) + + +func pending(text): + _output_type(types.pending, text) + + +func risky(text): + _output_type(types.risky, text) + + +func warn(text): + _output_type(types.warn, text) + + +func log(text='', fmt=fmts.none): + if(text == ''): + _output("\n") + else: + _log(text + "\n", fmt) + return null + + +func lograw(text, fmt=fmts.none): + return _output(text, fmt) + + +func p(p1='', p2='', p3='', p4='', p5='', p6='', p7='', p8='', p9='', p10='', ): + self.log(str(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)) + + +# Print the test name if we aren't skipping names of tests that pass (basically +# what _less_test_names means)) +func log_test_name(): + # suppress output if we haven't printed the test name yet and + # what to print is the test name. + if(!_less_test_names): + _print_test_name() + +# --------------- +# Misc +# --------------- +func get_gut(): + return _gut + + +func set_gut(gut): + _gut = gut + if(_gut == null): + _printers.gui = null + else: + if(_printers.gui == null): + _printers.gui = GutUtils.Printers.GutGuiPrinter.new() + + +func get_indent_level(): + return _indent_level + + +func set_indent_level(indent_level): + _indent_level = max(_min_indent_level, indent_level) + + +func get_indent_string(): + return _indent_string + + +func set_indent_string(indent_string): + _indent_string = indent_string + + +func clear(): + for key in _logs: + _logs[key].clear() + + +func inc_indent(): + _indent_level += 1 + + +func dec_indent(): + _indent_level = max(_min_indent_level, _indent_level -1) + + +func is_type_enabled(type): + return _type_data[type].enabled + + +func set_type_enabled(type, is_enabled): + _type_data[type].enabled = is_enabled + + +func get_less_test_names(): + return _less_test_names + + +func set_less_test_names(less_test_names): + _less_test_names = less_test_names + + +func disable_printer(name, is_disabled): + if(_printers[name] != null): + _printers[name].set_disabled(is_disabled) + + +func is_printer_disabled(name): + return _printers[name].get_disabled() + + +func disable_formatting(is_disabled): + for key in _printers: + _printers[key].set_format_enabled(!is_disabled) + + +func disable_all_printers(is_disabled): + for p in _printers: + disable_printer(p, is_disabled) + + +func get_printer(printer_key): + return _printers[printer_key] + + +func _yield_text_terminal(text): + var printer = _printers['terminal'] + if(_yield_calls != 0): + printer.clear_line() + printer.back(_last_yield_text.length()) + printer.send(text, fmts.yellow) + + +# Format and printing rules for the "Awaiting" messages. +func wait_msg(text): + if(_type_data.warn.enabled): + self.log(text, fmts.yellow) + + +func get_gui_bbcode(): + return _printers.gui.get_bbcode() + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# This class wraps around the various printers and supplies formatting for the +# various message types (error, warning, etc). +# ############################################################################## diff --git a/addons/gut/logger.gd.uid b/addons/gut/logger.gd.uid new file mode 100644 index 0000000..dc0510b --- /dev/null +++ b/addons/gut/logger.gd.uid @@ -0,0 +1 @@ +uid://cqd3rmuu2drug diff --git a/addons/gut/menu_manager.gd.uid b/addons/gut/menu_manager.gd.uid new file mode 100644 index 0000000..5a157a4 --- /dev/null +++ b/addons/gut/menu_manager.gd.uid @@ -0,0 +1 @@ +uid://b3k81q304cji1 diff --git a/addons/gut/method_maker.gd b/addons/gut/method_maker.gd new file mode 100644 index 0000000..540982d --- /dev/null +++ b/addons/gut/method_maker.gd @@ -0,0 +1,192 @@ +class MethodMakerCallParameters: + var p_name = null + var default = null + var vararg = false + + func _init(n,d): + p_name = n + default = d + + func get_signature(): + if(vararg): + return "...args: Array" + else: + return str(p_name, "=", default) + + + + +# ------------------------------------------------------------------------------ +# This class will generate method declaration lines based on method meta +# data. It will create defaults that match the method data. +# +# -------------------- +# function meta data +# -------------------- +# name: +# flags: +# args: [{ +# (class_name:), +# (hint:0), +# (hint_string:), +# (name:), +# (type:4), +# (usage:7) +# }] +# default_args [] +const PARAM_PREFIX = 'p_' + + +var _lgr = GutUtils.get_logger() +static var _func_template = GutUtils.get_file_as_text('res://addons/gut/double_templates/function_template.txt') +static var _init_template = GutUtils.get_file_as_text('res://addons/gut/double_templates/init_template.txt') + + +# ############### +# Private +# ############### + +func _make_stub_default(method, index): + return str('__gutdbl.default_val("', method, '",', index, ')') + + +func _make_arg_array(method_meta): + var to_return = [] + + for i in range(method_meta.args.size()): + var pname = method_meta.args[i].name + var dflt_text = _make_stub_default(method_meta.name, i) + to_return.append(MethodMakerCallParameters.new(PARAM_PREFIX + pname, dflt_text)) + + if(method_meta.flags & METHOD_FLAG_VARARG): + var cp = MethodMakerCallParameters.new("args", "") + cp.vararg = true + to_return.append(cp) + + return to_return + + +# Creates a list of parameters with defaults of null unless a default value is +# found in the metadata. If a default is found in the meta then it is used if +# it is one we know how support. +# +# If a default is found that we don't know how to handle then this method will +# return null. +func _get_arg_text(arg_array): + var text = '' + + for i in range(arg_array.size()): + text += arg_array[i].get_signature() + if(i != arg_array.size() -1): + text += ', ' + + return text + + +# creates a call to the function in meta in the super's class. +func _get_super_call_text(parsed_method, singleton): + var return_it = '' + if(parsed_method.return_type_text != 'void'): + return_it = 'return ' + + if parsed_method.meta.flags & MethodFlags.METHOD_FLAG_VIRTUAL_REQUIRED != 0: + if(return_it != ''): + return_it = '; return null' + return '__gutdbl.gut_ref.get_ref().get_logger().error(' + \ + '"Cannot call super() because method %s is abstract.")%s' \ + % [parsed_method.meta.name, return_it] + + var params = '' + for i in range(parsed_method.args.size()): + params += PARAM_PREFIX + parsed_method.args[i].name + if(i != parsed_method.args.size() -1): + params += ', ' + + if(singleton != null): + return str(return_it, 'await __gutdbl.get_singleton().', parsed_method.meta.name, '(', params, ')') + else: + return str(return_it, 'await super(', params, ')') + + +func _get_spy_call_parameters_text(args): + var called_with = 'null' + + if(args.size() > 0): + called_with = '[' + for i in range(args.size()): + called_with += args[i].p_name + if(i < args.size() - 1): + called_with += ', ' + called_with += ']' + + return called_with + + +func _get_init_text(meta, args, method_params, param_array): + var text = null + + var decleration = str('func ', meta.name, '(', method_params, ')') + var super_params = '' + if(args.size() > 0): + for i in range(args.size()): + super_params += args[i].p_name + if(i != args.size() -1): + super_params += ', ' + + text = _init_template.format({ + "func_decleration":decleration, + "super_params":super_params, + "param_array":param_array, + "method_name":meta.name, + }) + + return text + +# ############### +# Public +# ############### + + + +# Creates a delceration for a function based off of function metadata. All +# types whose defaults are supported will have their values. If a datatype +# is not supported and the parameter has a default, a warning message will be +# printed and the declaration will return null. +func get_function_text(parsed_method, singleton=null): + var meta = parsed_method.meta + var text = null + var args = _make_arg_array(meta) + var param_array = _get_spy_call_parameters_text(args) + var method_params = _get_arg_text(args); + + if(param_array == 'null'): + param_array = '[]' + + if(method_params != null): + if(meta.name == '_init'): + text = _get_init_text(meta, args, method_params, param_array) + else: + var return_it = '' + if(parsed_method.return_type_text != 'void'): + return_it = 'return ' + + var decleration = str('func ', meta.name, '(', method_params, '):') + text = _func_template.format({ + "func_decleration": decleration, + "method_name": meta.name, + "param_array": param_array, + "super_call": _get_super_call_text(parsed_method, singleton), + "return_it": return_it + }) + + return text + + +func get_logger(): + return _lgr + + +func set_logger(logger): + _lgr = logger + + diff --git a/addons/gut/method_maker.gd.uid b/addons/gut/method_maker.gd.uid new file mode 100644 index 0000000..e57102f --- /dev/null +++ b/addons/gut/method_maker.gd.uid @@ -0,0 +1 @@ +uid://c6fxaf7bsbrp7 diff --git a/addons/gut/one_to_many.gd b/addons/gut/one_to_many.gd new file mode 100644 index 0000000..652c0f7 --- /dev/null +++ b/addons/gut/one_to_many.gd @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------------ +# This datastructure represents a simple one-to-many relationship. It manages +# a dictionary of value/array pairs. It ignores duplicates of both the "one" +# and the "many". You can disable ignoring dupliates of the "many" via +# ignore_many_dupes. This setting is not retroactive and will only affect +# new calls to add. +# ------------------------------------------------------------------------------ +var items = {} +var ignore_many_dupes = true + +# return the size of items or the size of an element in items if "one" was +# specified. +func size(one=null): + var to_return = 0 + if(one == null): + to_return = items.size() + elif(items.has(one)): + to_return = items[one].size() + return to_return + + +# Add an element to "one" if it does not already exist +func add(one, many_item): + if(items.has(one)): + if(!ignore_many_dupes or !items[one].has(many_item)): + items[one].append(many_item) + else: + items[one] = [many_item] + + +func clear(): + items.clear() + + +func has(one, many_item): + var to_return = false + if(items.has(one)): + to_return = items[one].has(many_item) + return to_return + + +func to_s(): + var to_return = '' + for key in items: + to_return += str(key, ": ", items[key], "\n") + return to_return \ No newline at end of file diff --git a/addons/gut/one_to_many.gd.uid b/addons/gut/one_to_many.gd.uid new file mode 100644 index 0000000..62745fe --- /dev/null +++ b/addons/gut/one_to_many.gd.uid @@ -0,0 +1 @@ +uid://cy3q0wsdgq515 diff --git a/addons/gut/orphan_counter.gd b/addons/gut/orphan_counter.gd new file mode 100644 index 0000000..ea9057c --- /dev/null +++ b/addons/gut/orphan_counter.gd @@ -0,0 +1,221 @@ +# ------------------------------------------------------------------------------ +# It keeps track of the orphans...so this is best name it could ever have. +# ------------------------------------------------------------------------------ +class GutOrphanage: + const UNGROUPED = "Outside Tests" + const SUBGROUP_SEP = '->' + + var orphan_ids = {} + var oprhans_by_group = {} + var strutils = GutUtils.Strutils.new() + + # wrapper for stubbing + func _get_system_orphan_node_ids(): + return Node.get_orphan_node_ids() + + + func _make_group_key(group=null, subgroup=null): + var to_return = UNGROUPED + if(group != null): + to_return = group + + if(subgroup == null): + to_return += str(SUBGROUP_SEP, UNGROUPED) + else: + to_return += str(SUBGROUP_SEP, subgroup) + + return to_return + + + func _add_orphan_by_group(id, group, subgroup): + var key = _make_group_key(group, subgroup) + if(oprhans_by_group.has(key)): + oprhans_by_group[key].append(id) + else: + oprhans_by_group[key] = [id] + + + func process_orphans(group=null, subgroup=null): + var new_orphans = [] + for orphan_id in _get_system_orphan_node_ids(): + if(!orphan_ids.has(orphan_id)): + new_orphans.append(orphan_id) + orphan_ids[orphan_id] = { + "group":GutUtils.nvl(group, UNGROUPED), + "subgroup":GutUtils.nvl(subgroup, UNGROUPED), + "instance":instance_from_id(orphan_id) + } + _add_orphan_by_group(orphan_id, group, subgroup) + + return new_orphans + + + func get_orphan_ids(group=null, subgroup=null): + var key = _make_group_key(group, subgroup) + return oprhans_by_group.get(key, []) + + + # Given the likely size, this was way easier than making a dictionary + # of dictionaries of arrays. + func get_all_group_orphans(group): + var to_return = [] + for key in oprhans_by_group: + if(key == group or key.begins_with(str(group, SUBGROUP_SEP))): + to_return.append_array(oprhans_by_group[key]) + return to_return + + + # clears out anything that is not still an orphan. + func clean(): + oprhans_by_group.clear() + for key in orphan_ids.keys(): + var inst = orphan_ids[key].instance + if(!is_instance_valid(inst) or inst.get_parent() != null and not orphan_ids.has(inst.get_parent().get_instance_id())): + orphan_ids.erase(key) + else: + _add_orphan_by_group(key, orphan_ids[key].group, orphan_ids[key].subgroup) + + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +var _strutils = GutStringUtils.new() + +var orphanage : GutOrphanage = GutOrphanage.new() +var logger = GutUtils.get_logger() +var autofree = GutUtils.AutoFree.new() + + +func _count_all_children(instance): + var count = instance.get_child_count() + for child in instance.get_children(): + count += _count_all_children(child) + return count + + +func get_orphan_list_text(orphan_ids): + var text = "" + for id in orphan_ids: + var kid_count_text = '' + var inst = orphanage.orphan_ids[id].instance + if(is_instance_valid(inst) and inst.get_parent() == null): + var kid_count = _count_all_children(inst) + if(kid_count != 0): + kid_count_text = str(' + ', kid_count) + + var autofree_text = '' + if(autofree.has_instance_id(id)): + autofree_text = (" (autofree)") + + if(text != ''): + text += "\n" + text += str('* [', _strutils.type2str(inst), ']', kid_count_text, autofree_text) + + return text + + +func orphan_count() -> int: + return int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)) + + +func record_orphans(group, subgroup = null): + return orphanage.process_orphans(group, subgroup) + + +func convert_instance_ids_to_valid_instances(instance_ids): + var to_return = [] + for entry in instance_ids: + if(is_instance_id_valid(entry)): + to_return.append(instance_from_id(entry)) + return to_return + + +func end_script(script_path, should_log): + record_orphans(script_path) + var orphans = orphanage.get_all_group_orphans(script_path) + if(orphans.size() > 0 and should_log): + logger.orphan(str(orphans.size(), ' orphans')) + + +func end_test(script_path, test_name, should_log = true): + record_orphans(script_path, test_name) + orphanage.clean() + # Must get all the orphans and not just the results of record_orphans + # because record_orphans may have been called for this group/subgroup + # already. + var orphans = get_orphan_ids(script_path, test_name) + if(orphans.size() > 0 and should_log): + logger.orphan(str(orphans.size(), ' Orphans')) + logger.inc_indent() + logger.orphan(get_orphan_list_text(orphans)) + logger.dec_indent() + + +func get_orphan_ids(group=null, subgroup=null): + var ids = [] + if(group == null): + ids = orphanage.orphan_ids.keys() + elif(subgroup == null): + ids = orphanage.get_all_group_orphans(group) + else: + ids = orphanage.get_orphan_ids(group, subgroup) + + return ids + + +func get_count() -> int: + return orphan_count() + + +func log_all(): + var last_script = '' + var last_test = '' + + for id in orphanage.orphan_ids: + var entry = orphanage.orphan_ids[id] + + if(last_script != entry.group): + last_script = entry.group + last_test = '' + logger.log(entry.group) + + if(last_test != entry.subgroup): + logger.inc_indent() + logger.log(str('- ', entry.subgroup)) + last_test = entry.subgroup + logger.inc_indent() + var orphan_ids = orphanage.get_orphan_ids(last_script, last_test) + logger.orphan(get_orphan_list_text(orphan_ids)) + logger.dec_indent() + logger.dec_indent() + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/orphan_counter.gd.uid b/addons/gut/orphan_counter.gd.uid new file mode 100644 index 0000000..e67f4a2 --- /dev/null +++ b/addons/gut/orphan_counter.gd.uid @@ -0,0 +1 @@ +uid://cgoysoitlbudy diff --git a/addons/gut/parameter_factory.gd b/addons/gut/parameter_factory.gd new file mode 100644 index 0000000..906545a --- /dev/null +++ b/addons/gut/parameter_factory.gd @@ -0,0 +1,91 @@ +## Creates parameter structures for parameterized tests. +## +## This is a static class accessible in a [GutTest] script through +## [member GutTest.ParameterFactory]. It contains methods for constructing parameters to be +## used in parameterized tests. It currently only has one, if you have anyu +## ideas for more, make an issue. More of them would be great since I prematurely +## decided to make this static class and it has such a long name. I'd feel a lot +## better about it if there was more in here. +## [br] +## Additional Helper Ideas?[br] +## [li]File. IDK what it would look like. csv maybe.[/li] +## [li]Random values within a range?[/li] +## [li]All int values in a range or add an optioanal step.[/li] + + + + +## Creates an array of dictionaries. It pairs up the names array with each set +## of values in values. If more names than values are specified then the missing +## values will be filled with nulls. If more values than names are specified +## those values will be ignored. +## +## Example: +##[codeblock] +## create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) returns +## [{a:1, b:2}, {a:'one', b:'two'}] +##[/codeblock] +## [br] +## This allows you to increase readability of your parameterized tests: +## [br] +##[codeblock] +## var params = create_named_parameters(['a', 'b'], [[1, 2], ['one', 'two']]) +## func test_foo(p = use_parameters(params)): +## assert_eq(p.a, p.b) +##[/codeblock] +## [br] +## Parameters:[br] +##[li]names: an array of names to be used as keys in the dictionaries[/li] +##[li]values: an array of arrays of values.[/li] +static func named_parameters(names, values): + var named = [] + for i in range(values.size()): + var entry = {} + + var parray = values[i] + if(typeof(parray) != TYPE_ARRAY): + parray = [values[i]] + + for j in range(names.size()): + if(j >= parray.size()): + entry[names[j]] = null + else: + entry[names[j]] = parray[j] + named.append(entry) + + return named + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# This is the home for all parameter creation helpers. These functions should +# all return an array of values to be used as parameters for parameterized +# tests. +# ############################################################################## \ No newline at end of file diff --git a/addons/gut/parameter_factory.gd.uid b/addons/gut/parameter_factory.gd.uid new file mode 100644 index 0000000..c02e808 --- /dev/null +++ b/addons/gut/parameter_factory.gd.uid @@ -0,0 +1 @@ +uid://c0e08oi55x8qj diff --git a/addons/gut/parameter_handler.gd b/addons/gut/parameter_handler.gd new file mode 100644 index 0000000..794d693 --- /dev/null +++ b/addons/gut/parameter_handler.gd @@ -0,0 +1,36 @@ +var _params = null +var _call_count = 0 +var _logger = null + +func _init(params=null): + _params = params + _logger = GutUtils.get_logger() + if(typeof(_params) != TYPE_ARRAY): + _logger.error('You must pass an array to parameter_handler constructor.') + _params = null + + +func next_parameters(): + _call_count += 1 + return _params[_call_count -1] + +func get_current_parameters(): + return _params[_call_count] + +func is_done(): + var done = true + if(_params != null): + done = _call_count == _params.size() + return done + +func get_logger(): + return _logger + +func set_logger(logger): + _logger = logger + +func get_call_count(): + return _call_count + +func get_parameter_count(): + return _params.size() diff --git a/addons/gut/parameter_handler.gd.uid b/addons/gut/parameter_handler.gd.uid new file mode 100644 index 0000000..355ad4f --- /dev/null +++ b/addons/gut/parameter_handler.gd.uid @@ -0,0 +1 @@ +uid://ba87ra5ep18wa diff --git a/addons/gut/plugin.cfg b/addons/gut/plugin.cfg new file mode 100644 index 0000000..64cd593 --- /dev/null +++ b/addons/gut/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Gut" +description="Unit Testing tool for Godot." +author="Butch Wesley" +version="9.7.1" +script="gut_plugin.gd" diff --git a/addons/gut/printers.gd b/addons/gut/printers.gd new file mode 100644 index 0000000..9742356 --- /dev/null +++ b/addons/gut/printers.gd @@ -0,0 +1,184 @@ +# ------------------------------------------------------------------------------ +# Interface and some basic functionality for all printers. +# ------------------------------------------------------------------------------ +class GutPrinter: + var _format_enabled = true + var _disabled = false + var _printer_name = 'NOT SET' + var _show_name = false # used for debugging, set manually + + func get_format_enabled(): + return _format_enabled + + func set_format_enabled(format_enabled): + _format_enabled = format_enabled + + func send(text, fmt=null): + if(_disabled): + return + + var formatted = text + if(fmt != null and _format_enabled): + formatted = format_text(text, fmt) + + if(_show_name): + formatted = str('(', _printer_name, ')') + formatted + + _output(formatted) + + func get_disabled(): + return _disabled + + func set_disabled(disabled): + _disabled = disabled + + # -------------------- + # Virtual Methods (some have some default behavior) + # -------------------- + func _output(text): + pass + + func format_text(text, fmt): + return text + +# ------------------------------------------------------------------------------ +# Responsible for sending text to a GUT gui. +# ------------------------------------------------------------------------------ +class GutGuiPrinter: + extends GutPrinter + var _textbox = null + + var _colors = { + red = Color.RED, + yellow = Color.YELLOW, + green = Color.GREEN, + blue = Color.BLUE + } + + func _init(): + _printer_name = 'gui' + + func _wrap_with_tag(text, tag): + return str('[', tag, ']', text, '[/', tag, ']') + + func _color_text(text, c_word): + return '[color=' + c_word + ']' + text + '[/color]' + + # Remember, we have to use push and pop because the output from the tests + # can contain [] in it which can mess up the formatting. There is no way + # as of 3.4 that you can get the bbcode out of RTL when using push and pop. + # + # The only way we could get around this is by adding in non-printable + # whitespace after each "[" that is in the text. Then we could maybe do + # this another way and still be able to get the bbcode out, or generate it + # at the same time in a buffer (like we tried that one time). + # + # Since RTL doesn't have good search and selection methods, and those are + # really handy in the editor, it isn't worth making bbcode that can be used + # there as well. + # + # You'll try to get it so the colors can be the same in the editor as they + # are in the output. Good luck, and I hope I typed enough to not go too + # far that rabbit hole before finding out it's not worth it. + func format_text(text, fmt): + if(_textbox == null): + return + + if(fmt == 'bold'): + _textbox.push_bold() + elif(fmt == 'underline'): + _textbox.push_underline() + elif(_colors.has(fmt)): + _textbox.push_color(_colors[fmt]) + else: + # just pushing something to pop. + _textbox.push_normal() + + _textbox.add_text(text) + _textbox.pop() + + return '' + + func _output(text): + if(_textbox == null): + return + + _textbox.add_text(text) + + func get_textbox(): + return _textbox + + func set_textbox(textbox): + _textbox = textbox + + # This can be very very slow when the box has a lot of text. + func clear_line(): + _textbox.remove_line(_textbox.get_line_count() - 1) + _textbox.queue_redraw() + + func get_bbcode(): + return _textbox.text + + func get_disabled(): + return _disabled and _textbox != null + +# ------------------------------------------------------------------------------ +# This AND TerminalPrinter should not be enabled at the same time since it will +# result in duplicate output. printraw does not print to the console so i had +# to make another one. +# ------------------------------------------------------------------------------ +class GutConsolePrinter: + extends GutPrinter + var _buffer = '' + + func _init(): + _printer_name = 'console' + + # suppresses output until it encounters a newline to keep things + # inline as much as possible. + func _output(text): + if(text.ends_with("\n")): + print(_buffer + text.left(text.length() -1)) + _buffer = '' + else: + _buffer += text + +# ------------------------------------------------------------------------------ +# Prints text to terminal, formats some words. +# ------------------------------------------------------------------------------ +class GutTerminalPrinter: + extends GutPrinter + + var escape = PackedByteArray([0x1b]).get_string_from_ascii() + var cmd_colors = { + red = escape + '[31m', + yellow = escape + '[33m', + green = escape + '[32m', + blue = escape + '[34m', + + underline = escape + '[4m', + bold = escape + '[1m', + + default = escape + '[0m', + + clear_line = escape + '[2K' + } + + func _init(): + _printer_name = 'terminal' + + func _output(text): + # Note, printraw does not print to the console. + printraw(text) + + func format_text(text, fmt): + return cmd_colors[fmt] + text + cmd_colors.default + + func clear_line(): + send(cmd_colors.clear_line) + + func back(n): + send(escape + str('[', n, 'D')) + + func forward(n): + send(escape + str('[', n, 'C')) diff --git a/addons/gut/printers.gd.uid b/addons/gut/printers.gd.uid new file mode 100644 index 0000000..9880ad2 --- /dev/null +++ b/addons/gut/printers.gd.uid @@ -0,0 +1 @@ +uid://nijvqplhkwjc diff --git a/addons/gut/result_exporter.gd b/addons/gut/result_exporter.gd new file mode 100644 index 0000000..ee5a5ae --- /dev/null +++ b/addons/gut/result_exporter.gd @@ -0,0 +1,125 @@ +# ------------------------------------------------------------------------------ +# Creates a structure that contains all the data about the results of running +# tests. This was created to make an intermediate step organizing the result +# of a run and exporting it in a specific format. This can also serve as a +# unofficial GUT export format. +# ------------------------------------------------------------------------------ +var json = JSON.new() +var strutils = GutStringUtils.new() + +func _export_tests(gut, collected_script): + var to_return = {} + var tests = collected_script.tests + for test in tests: + if(test.get_status_text() != GutUtils.TEST_STATUSES.NOT_RUN): + var orphans = gut.get_orphan_counter().get_orphan_ids( + collected_script.get_filename_and_inner(), + test.name) + var orphan_node_strings = [] + for o in orphans: + if(is_instance_id_valid(o)): + orphan_node_strings.append(strutils.type2str(instance_from_id(o))) + + to_return[test.name] = { + "status":test.get_status_text(), + "passing":test.pass_texts, + "failing":test.fail_texts, + "pending":test.pending_texts, + "orphan_count":orphan_node_strings.size(), + "orphans":orphan_node_strings, + "time_taken": test.time_taken + } + + return to_return + +# TODO +# errors +func _export_scripts(gut): + var collector = gut.get_test_collector() + if(collector == null): + return {} + + var scripts = {} + + for s in collector.scripts: + var test_data = _export_tests(gut, s) + scripts[s.get_full_name()] = { + 'props':{ + "tests":test_data.keys().size(), + "pending":s.get_pending_count(), + "failures":s.get_fail_count(), + "skipped":s.was_skipped, + }, + "tests":test_data + } + return scripts + +func _make_results_dict(): + var result = { + 'test_scripts':{ + "props":{ + "pending":0, + "failures":0, + "passing":0, + "tests":0, + "time":0, + "orphans":0, + "errors":0, + "warnings":0, + "risky":0 + }, + "scripts":[] + } + } + return result + + +func get_results_dictionary(gut, include_scripts=true): + var scripts = [] + + if(include_scripts): + scripts = _export_scripts(gut) + + var result = _make_results_dict() + + var totals = gut.get_summary().get_totals() + + var props = result.test_scripts.props + props.pending = totals.pending + props.failures = totals.failing_tests + props.passing = totals.passing_tests + props.tests = totals.tests + props.errors = gut.logger.get_errors().size() + props.warnings = gut.logger.get_warnings().size() + props.time = gut.get_elapsed_time() + props.orphans = gut.get_orphan_counter().get_count() + props.risky = totals.risky + + result.test_scripts.scripts = scripts + + return result + + +func write_json_file(gut, path): + var dict = get_results_dictionary(gut) + var json_text = JSON.stringify(dict, ' ') + + var f_result = GutUtils.write_file(path, json_text) + if(f_result != OK): + var msg = str("Error: ", f_result, ". Could not create export file ", path) + GutUtils.get_logger().error(msg) + + return f_result + + + +func write_summary_file(gut, path): + var dict = get_results_dictionary(gut, false) + var json_text = JSON.stringify(dict, ' ') + + var f_result = GutUtils.write_file(path, json_text) + if(f_result != OK): + var msg = str("Error: ", f_result, ". Could not create export file ", path) + GutUtils.get_logger().error(msg) + + return f_result diff --git a/addons/gut/result_exporter.gd.uid b/addons/gut/result_exporter.gd.uid new file mode 100644 index 0000000..7044832 --- /dev/null +++ b/addons/gut/result_exporter.gd.uid @@ -0,0 +1 @@ +uid://bbto5glvw8efv diff --git a/addons/gut/script_parser.gd b/addons/gut/script_parser.gd new file mode 100644 index 0000000..ffde399 --- /dev/null +++ b/addons/gut/script_parser.gd @@ -0,0 +1,339 @@ +# These methods didn't have flags that would exclude them from being used +# in a double and they appear to break things if they are included. +const BLACKLIST = [ + 'get_script', + 'has_method', + '_to_string', +] + + +# ------------------------------------------------------------------------------ +# Combines the meta for the method with additional information. +# * flag for whether the method is local +# * adds a 'default' property to all parameters that can be easily checked per +# parameter +# ------------------------------------------------------------------------------ +class GutParsedMethod: + const NO_DEFAULT = '__no__default__' + + var _meta = {} + var meta = _meta : + get: return _meta + set(val): return; + + var is_local = false + var args = [] + var return_type_text = 'void' + + func _init(metadata): + _meta = metadata + var start_default = _meta.args.size() - _meta.default_args.size() + for i in range(_meta.args.size()): + var arg = _meta.args[i] + # Add a "default" property to the metadata so we don't have to do + # weird default paramter position math again. + if(i >= start_default): + arg['default'] = _meta.default_args[start_default - i] + else: + arg['default'] = NO_DEFAULT + args.append(arg) + + return_type_text = _get_return_type(metadata) + + func _get_return_type(meta): + var r_meta = meta["return"] + var return_keyword = GutConstants.TYPE_KEYWORDS[r_meta.type] + + if(r_meta.type != 0): + return_keyword = return_keyword + elif(r_meta.usage & PROPERTY_USAGE_NIL_IS_VARIANT != 0): + return_keyword = 'Variant' + else: + return_keyword = 'void' + + return return_keyword + + + func is_eligible_for_doubling(): + var has_bad_flag = _meta.flags & \ + (METHOD_FLAG_OBJECT_CORE | METHOD_FLAG_VIRTUAL | METHOD_FLAG_STATIC) + return !has_bad_flag and BLACKLIST.find(_meta.name) == -1 + + + func is_accessor(): + return _meta.name.begins_with('@') and \ + (_meta.name.ends_with('_getter') or _meta.name.ends_with('_setter')) + + + func to_s(): + var s = _meta.name + "(" + + for i in range(_meta.args.size()): + var arg = _meta.args[i] + if(str(arg.default) != NO_DEFAULT): + var val = str(arg.default) + if(val == ''): + val = '""' + s += str(arg.name, ' = ', val) + else: + s += str(arg.name) + + if(i != _meta.args.size() -1): + s += ', ' + + s += ")" + return s + + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GutParsedScript: + # All methods indexed by name. + var _methods_by_name = {} + + var _script_path = null + var script_path = _script_path : + get: return _script_path + set(val): return; + + var _subpath = null + var subpath = null : + get: return _subpath + set(val): return; + + var _resource = null + var resource = null : + get: return _resource + set(val): return; + + + var _is_native = false + var is_native = _is_native: + get: return _is_native + set(val): return; + + var _native_methods = {} + var _native_class_name = "" + var _native_class = null + + + + func _init(script_or_inst, inner_class=null): + var to_load = script_or_inst + + if(GutUtils.is_native_class(to_load)): + _resource = to_load + _is_native = true + # TODO this could be done with ClassDB instead of making instance. + var inst = to_load.new() + _native_class = to_load + _native_class_name = inst.get_class() + _native_methods = inst.get_method_list() + if(!inst is RefCounted): + inst.free() + else: + if(!script_or_inst is Resource): + to_load = load(script_or_inst.get_script().get_path()) + + _script_path = to_load.resource_path + if(inner_class != null): + _subpath = _find_subpath(to_load, inner_class) + + if(inner_class == null): + _resource = to_load + else: + _resource = inner_class + to_load = inner_class + + _parse_methods(to_load) + + + func _print_flags(meta): + print(str(meta.name, ':').rpad(30), str(meta.flags).rpad(4), ' = ', GutUtils.dec2bistr(meta.flags, 10)) + + + func _get_native_methods(base_type): + var to_return = [] + if(base_type != null): + var source = str('extends ', base_type) + var inst = GutUtils.create_script_from_source(source).new() + to_return = inst.get_method_list() + if(! inst is RefCounted): + inst.free() + return to_return + + + func _parse_methods(thing): + var methods = [] + if(is_native): + methods = _native_methods.duplicate() + else: + var base_type = thing.get_instance_base_type() + methods = _get_native_methods(base_type) + + for m in methods: + var parsed = GutParsedMethod.new(m) + _methods_by_name[m.name] = parsed + # _init must always be included so that we can initialize + # double_tools + if(m.name == '_init'): + parsed.is_local = true + + + # This loop will overwrite all entries in _methods_by_name with the local + # method object so there is only ever one listing for a function with + # the right "is_local" flag. + if(!is_native): + methods = thing.get_script_method_list() + methods.reverse() + for m in methods: + var parsed_method = GutParsedMethod.new(m) + parsed_method.is_local = true + _methods_by_name[m.name] = parsed_method + + + func _find_subpath(parent_script, inner): + var const_map = parent_script.get_script_constant_map() + var consts = const_map.keys() + var const_idx = 0 + var found = false + var to_return = null + + while(const_idx < consts.size() and !found): + var key = consts[const_idx] + var const_val = const_map[key] + if(typeof(const_val) == TYPE_OBJECT): + if(const_val == inner): + found = true + to_return = key + else: + to_return = _find_subpath(const_val, inner) + if(to_return != null): + to_return = str(key, '.', to_return) + found = true + + const_idx += 1 + + return to_return + + + func get_method(name): + return _methods_by_name[name] + + + func get_super_method(name): + var to_return = get_method(name) + if(to_return.is_local): + to_return = null + + return to_return + + func get_local_method(name): + var to_return = get_method(name) + if(!to_return.is_local): + to_return = null + + return to_return + + + func get_sorted_method_names(): + var keys = _methods_by_name.keys() + keys.sort() + return keys + + + func get_local_method_names(): + var names = [] + for method in _methods_by_name: + if(_methods_by_name[method].is_local): + names.append(method) + + return names + + + func get_super_method_names(): + var names = [] + for method in _methods_by_name: + if(!_methods_by_name[method].is_local): + names.append(method) + + return names + + + func get_local_methods(): + var to_return = [] + for key in _methods_by_name: + var method = _methods_by_name[key] + if(method.is_local): + to_return.append(method) + return to_return + + + func get_super_methods(): + var to_return = [] + for key in _methods_by_name: + var method = _methods_by_name[key] + if(!method.is_local): + to_return.append(method) + return to_return + + + func get_extends_text(): + var text = null + if(is_native): + text = str("extends ", _native_class_name) + else: + text = str("extends '", _script_path, "'") + if(_subpath != null): + text += '.' + _subpath + return text + + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +var scripts = {} + + +func _get_instance_id(thing): + var inst_id = null + + if(GutUtils.is_native_class(thing)): + var id_str = str(thing).replace("<", '').replace(">", '').split('#')[1] + inst_id = id_str.to_int() + elif(typeof(thing) == TYPE_STRING): + if(FileAccess.file_exists(thing)): + inst_id = load(thing).get_instance_id() + else: + inst_id = thing.get_instance_id() + + return inst_id + + +func parse(thing, inner_thing=null): + var key = -1 + if(inner_thing == null): + key = _get_instance_id(thing) + else: + key = _get_instance_id(inner_thing) + + var parsed = null + + if(key != null): + if(scripts.has(key)): + parsed = scripts[key] + else: + var obj = instance_from_id(_get_instance_id(thing)) + var inner = null + if(inner_thing != null): + inner = instance_from_id(_get_instance_id(inner_thing)) + + if(obj is Resource or GutUtils.is_native_class(obj)): + parsed = GutParsedScript.new(obj, inner) + scripts[key] = parsed + + return parsed + diff --git a/addons/gut/script_parser.gd.uid b/addons/gut/script_parser.gd.uid new file mode 100644 index 0000000..ab60633 --- /dev/null +++ b/addons/gut/script_parser.gd.uid @@ -0,0 +1 @@ +uid://c4k82nmegjoec diff --git a/addons/gut/signal_watcher.gd b/addons/gut/signal_watcher.gd new file mode 100644 index 0000000..0ffc1f6 --- /dev/null +++ b/addons/gut/signal_watcher.gd @@ -0,0 +1,218 @@ +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## + +# Some arbitrary string that should never show up by accident. If it does, then +# shame on you. +const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' + +# This hash holds the objects that are being watched, the signals that are being +# watched, and an array of arrays that contains arguments that were passed +# each time the signal was emitted. +# +# For example: +# _watched_signals => { +# ref1 => { +# 'signal1' => [[], [], []], +# 'signal2' => [[p1, p2]], +# 'signal3' => [[p1]] +# }, +# ref2 => { +# 'some_signal' => [], +# 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]] +# } +# } +# +# In this sample: +# - signal1 on the ref1 object was emitted 3 times and each time, zero +# parameters were passed. +# - signal3 on ref1 was emitted once and passed a single parameter +# - some_signal on ref2 was never emitted. +# - other_signal on ref2 was emitted 3 times, each time with 3 parameters. +var _watched_signals = {} +var _lgr = GutUtils.get_logger() + +func _add_watched_signal(obj, name): + # SHORTCIRCUIT - ignore dupes + if(_watched_signals.has(obj) and _watched_signals[obj].has(name)): + return + + if(!_watched_signals.has(obj)): + _watched_signals[obj] = {name:[]} + else: + _watched_signals[obj][name] = [] + obj.connect(name,Callable(self,'_on_watched_signal').bind(obj,name)) + +# This handles all the signals that are watched. It supports up to 9 parameters +# which could be emitted by the signal and the two parameters used when it is +# connected via watch_signal. I chose 9 since you can only specify up to 9 +# parameters when dynamically calling a method via call (per the Godot +# documentation, i.e. some_object.call('some_method', 1, 2, 3...)). +# +# Based on the documentation of emit_signal, it appears you can only pass up +# to 4 parameters when firing a signal. I haven't verified this, but this should +# future proof this some if the value ever grows. +func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \ + arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \ + arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \ + arg10=ARG_NOT_SET, arg11=ARG_NOT_SET): + var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11] + + # strip off any unused vars. + var idx = args.size() -1 + while(str(args[idx]) == ARG_NOT_SET): + args.remove_at(idx) + idx -= 1 + + # retrieve object and signal name from the array and remove_at them. These + # will always be at the end since they are added when the connect happens. + var signal_name = args[args.size() -1] + args.pop_back() + var object = args[args.size() -1] + args.pop_back() + + if(_watched_signals.has(object)): + _watched_signals[object][signal_name].append(args) + else: + _lgr.error(str("signal_watcher._on_watched_signal: Got signal for unwatched object: ", object, '::', signal_name)) + +# This parameter stuff should go into test.gd not here. This thing works +# just fine the way it is. +func _obj_name_pair(obj_or_signal, signal_name=null): + var to_return = { + 'object' : obj_or_signal, + 'signal_name' : signal_name + } + if(obj_or_signal is Signal): + to_return.object = obj_or_signal.get_object() + to_return.signal_name = obj_or_signal.get_name() + + return to_return + + +func does_object_have_signal(object, signal_name): + var signals = object.get_signal_list() + for i in range(signals.size()): + if(signals[i]['name'] == signal_name): + return true + return false + +func watch_signals(object): + var signals = object.get_signal_list() + for i in range(signals.size()): + _add_watched_signal(object, signals[i]['name']) + +func watch_signal(object, signal_name): + var did = false + if(does_object_have_signal(object, signal_name)): + _add_watched_signal(object, signal_name) + did = true + else: + GutUtils.get_logger().warn(str(object, ' does not have signal ', signal_name)) + return did + +func get_emit_count(object, signal_name): + var to_return = -1 + if(is_watching(object, signal_name)): + to_return = _watched_signals[object][signal_name].size() + return to_return + +func did_emit(object, signal_name=null): + var vals = _obj_name_pair(object, signal_name) + var did = false + if(is_watching(vals.object, vals.signal_name)): + did = get_emit_count(vals.object, vals.signal_name) != 0 + return did + +func print_object_signals(object): + var list = object.get_signal_list() + for i in range(list.size()): + print(list[i].name, "\n ", list[i]) + +func get_signal_parameters(object, signal_name, index=-1): + var params = null + if(is_watching(object, signal_name)): + var all_params = _watched_signals[object][signal_name] + if(all_params.size() > 0): + if(index == -1): + index = all_params.size() -1 + params = all_params[index] + return params + +func is_watching_object(object): + return _watched_signals.has(object) + +func is_watching(object, signal_name): + return _watched_signals.has(object) and _watched_signals[object].has(signal_name) + +func clear(): + for obj in _watched_signals: + if(GutUtils.is_not_freed(obj)): + for signal_name in _watched_signals[obj]: + obj.disconnect(signal_name, Callable(self,'_on_watched_signal')) + _watched_signals.clear() + +# Returns a list of all the signal names that were emitted by the object. +# If the object is not being watched then an empty list is returned. +func get_signals_emitted(obj): + var emitted = [] + if(is_watching_object(obj)): + for signal_name in _watched_signals[obj]: + if(_watched_signals[obj][signal_name].size() > 0): + emitted.append(signal_name) + + return emitted + + +func get_signal_summary(obj): + var emitted = {} + if(is_watching_object(obj)): + for signal_name in _watched_signals[obj]: + if(_watched_signals[obj][signal_name].size() > 0): + # maybe this could return parameters if any were sent. should + # have an empty list if no parameters were ever sent to the + # signal. Or this all just gets moved into print_signal_summary + # since this wouldn't be that useful without more data in the + # summary. + var entry = { + emit_count = get_emit_count(obj, signal_name) + } + emitted[signal_name] = entry + + return emitted + + +func print_signal_summary(obj): + if(!is_watching_object(obj)): + var msg = str('Not watching signals for ', obj) + GutUtils.get_logger().warn(msg) + return + + var summary = get_signal_summary(obj) + print(obj, '::Signals') + var sorted = summary.keys() + sorted.sort() + for key in sorted: + print(' - ', key, ' x ', summary[key].emit_count) diff --git a/addons/gut/signal_watcher.gd.uid b/addons/gut/signal_watcher.gd.uid new file mode 100644 index 0000000..f2ca904 --- /dev/null +++ b/addons/gut/signal_watcher.gd.uid @@ -0,0 +1 @@ +uid://yj7vo3wcr68q diff --git a/addons/gut/singleton_parser.gd b/addons/gut/singleton_parser.gd new file mode 100644 index 0000000..2c5f002 --- /dev/null +++ b/addons/gut/singleton_parser.gd @@ -0,0 +1,88 @@ +class GutParsedSingleton: + var methods_by_name = {} + var enums = {} + var properties = {} + var signals = {} + var singleton_name = 'unknown' + var singleton_id = -1 + var base_singleton = null + + func _init(singleton): + base_singleton = singleton + var sname = singleton.get_class() + singleton_name = sname + singleton_id = singleton.get_instance_id() + + for method in ClassDB.class_get_method_list(sname, true): + var pm = GutUtils.ScriptCollector.GutParsedMethod.new(method) + methods_by_name[method.name] = pm + + for e in ClassDB.class_get_enum_list(sname, true): + for c in ClassDB.class_get_enum_constants(sname, e): + enums[c] = singleton.get(c) + + # Some singletons have integer constants that are not in the enum list. + # All enum constants appear to be integer constants, but I don't want + # to assume that will always be true. + for c in ClassDB.class_get_integer_constant_list(sname, true): + # just overwrite existing or add, seems faster than checking if it + # exists first. + enums[c] = singleton.get(c) + + for p in ClassDB.class_get_property_list(sname, true): + properties[p.name] = singleton.get(p.name) + + for s in ClassDB.class_get_signal_list(sname, true): + signals[s.name] = s + + + func get_signal_text(signal_meta): + var text = "" + for arg in signal_meta.args: + if(text.length() > 0): + text += ", " + text += arg.name + + return str('signal ', signal_meta.name, '(', text, ')') + + + func get_all_signal_text(): + var text = '' + for key in signals: + if(text.length() > 0): + text += "\n" + text += get_signal_text(signals[key]) + return text + + + func get_all_constants_text(): + var text = "" + for key in enums: + text += str('const ', key, ' = ', enums[key], "\n") + return text + + + func get_all_properties_text(): + var text = "" + # This defaults values to what the singleton is currently set to. This was + # easier than remembering how to turn the defaults in the meta into code. + # This might be the wrong choice. + for key in properties: + # AudioServer had a property in the meta named "Fallback values" and I + # don't know what it is, so I'm ignoring all properties with a space in + # the name. + if(key.find(" ") == -1): + text += str("var ", key, " = ", singleton_name, ".", key, "\n") + return text + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +var singletons = {} + +func parse(singleton): + if(!singletons.has(singleton)): + singletons[singleton] = GutParsedSingleton.new(singleton) + + return singletons[singleton] \ No newline at end of file diff --git a/addons/gut/singleton_parser.gd.uid b/addons/gut/singleton_parser.gd.uid new file mode 100644 index 0000000..74a6ff4 --- /dev/null +++ b/addons/gut/singleton_parser.gd.uid @@ -0,0 +1 @@ +uid://dpjkkxkx85l3e diff --git a/addons/gut/source_code_pro.fnt b/addons/gut/source_code_pro.fnt new file mode 100644 index 0000000000000000000000000000000000000000..eb6b9b859954c85bc878e93e6893d6f552b01a9e GIT binary patch literal 42799 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' or + typeof(thing) != TYPE_OBJECT or + GutUtils.is_double(thing)): + return + + if(thing.get_script() == null): + if(thing is PackedScene): + filename = _get_filename(thing.resource_path) + else: + # If it isn't a packed scene and it doesn't have a script then + # we do nothing. This just reads better. + pass + elif(!GutUtils.is_native_class(thing)): + if(GutUtils.is_inner_class(thing.get_script())): + filename = GutUtils.inner_class_registry.get_full_path(thing.get_script()) + else: + filename = thing.get_script().resource_path.get_file() + + return filename + +# ------------------------------------------------------------------------------ +# Better object/thing to string conversion. Includes extra details about +# whatever is passed in when it can/should. +# ------------------------------------------------------------------------------ +func type2str(thing): + var filename = _get_obj_filename(thing) + var str_thing = str(thing) + + if(thing == null): + # According to str there is a difference between null and an Object + # that is somehow null. To avoid getting '[Object:null]' as output + # always set it to str(null) instead of str(thing). A null object + # will pass typeof(thing) == TYPE_OBJECT check so this has to be + # before that. + str_thing = str(null) + elif(typeof(thing) == TYPE_FLOAT): + if(!'.' in str_thing): + str_thing += '.0' + elif(typeof(thing) == TYPE_STRING): + str_thing = str('"', thing, '"') + elif(typeof(thing) in _str_ignore_types): + # do nothing b/c we already have str(thing) in + # to_return. I think this just reads a little + # better this way. + pass + elif(typeof(thing) == TYPE_OBJECT): + if(GutUtils.is_native_class(thing)): + str_thing = GutUtils.get_native_class_name(thing) + elif(GutUtils.is_double(thing)): + var double_path = _get_filename(thing.__gutdbl.thepath) + if(thing.__gutdbl.subpath != ''): + double_path += str('/', thing.__gutdbl.subpath) + elif(thing.__gutdbl.singleton_name != ''): + double_path = thing.__gutdbl.singleton_name + " Singleton" + + var double_type = "double" + if(thing.__gutdbl.is_partial): + double_type = "partial-double" + + str_thing += str("(", double_type, " of ", double_path, ")") + + filename = null + elif(types.has(typeof(thing))): + if(!str_thing.begins_with('(')): + str_thing = '(' + str_thing + ')' + str_thing = str(types[typeof(thing)], str_thing) + + if(filename != null): + str_thing += str('(', filename, ')') + return str_thing + +# ------------------------------------------------------------------------------ +# Returns the string truncated with an '...' in it. Shows the start and last +# 10 chars. If the string is smaller than max_size the entire string is +# returned. If max_size is -1 then truncation is skipped. +# ------------------------------------------------------------------------------ +func truncate_string(src, max_size): + var to_return = src + if(src.length() > max_size - 10 and max_size != -1): + to_return = str(src.substr(0, max_size - 10), '...', src.substr(src.length() - 10, src.length())) + return to_return + + +## Indents multiline text. Indents every line of [param text] with [param times] +## copies of the String [param pad]. If the string ends with a newline, +## no indent is added after that newline character and the resulting string +## will still end on a newline. +func indent_text(text: String, times: int, pad: String) -> String: + if(times == 0): + return text + + var to_return := text + var ending_newline := '' + + if(text.ends_with("\n")): + ending_newline = "\n" + to_return = to_return.left(-1) + + var padding := pad.repeat(times) + to_return = to_return.replace("\n", "\n" + padding) + to_return += ending_newline + + return padding + to_return diff --git a/addons/gut/strutils.gd.uid b/addons/gut/strutils.gd.uid new file mode 100644 index 0000000..87b98e9 --- /dev/null +++ b/addons/gut/strutils.gd.uid @@ -0,0 +1 @@ +uid://dntjtiq2ppvmq diff --git a/addons/gut/stub_params.gd b/addons/gut/stub_params.gd new file mode 100644 index 0000000..8c7157a --- /dev/null +++ b/addons/gut/stub_params.gd @@ -0,0 +1,240 @@ +var _is_return_override = false +var _is_defaults_override = false +var _is_call_override = false +var _method_meta : Dictionary = {} + + +var _lgr = GutUtils.get_logger() +var logger = _lgr : + get: return _lgr + set(val): _lgr = val + + +var return_val = GutConstants.NOT_SET : + get(): + if(GutConstants.is_not_set(return_val)): + return null + else: + return return_val +var return_type = TYPE_NIL +var stub_target = null +var parameters = null # the parameter values to match method call on. +var stub_method = null +var call_super = false +var call_this = null +var locked = false + +# When this stub is a parameter stub, this indicates that these are the defaults +# defined in the script +# When this stub is an action stub, this indicates it is a default stub added +# by the stubber. This is currently used to stub native methods to call super +# by default, but still be able to override that stub with any other stub. +var is_script_default = false + +var parameter_count = -1 : + get(): + _lgr.deprecated("parameter count deprecated") + return -1 + +# Default values for parameters. This is used to store default values for +# scripts and to override those values. I'm not sure if there is a need to +# override them anymore, since I think this was introduced for stubbing vararg +# methods, but you still can for now. This value should only be used if +# is_defaults_override is true. +var parameter_defaults = [] + +const NOT_SET = '|_1_this_is_not_set_1_|' + +func _init(target=null, method=null, _subpath=null): + stub_target = target + stub_method = method + + if(typeof(target) == TYPE_CALLABLE): + stub_target = target.get_object() + stub_method = target.get_method() + parameters = target.get_bound_arguments() + if(parameters.size() == 0): + parameters = null + elif(typeof(target) == TYPE_STRING): + if(target.is_absolute_path()): + stub_target = load(str(target)) + else: + _lgr.warn(str(target, ' is not a valid path')) + + if(stub_target is PackedScene): + stub_target = GutUtils.get_scene_script_object(stub_target) + + # this is used internally to stub default parameters for everything that is + # doubled...or something. Look for stub_defaults_from_meta for usage. This + # behavior is not to be used by end users. + if(typeof(method) == TYPE_DICTIONARY): + _method_meta = method + _load_defaults_from_metadata(method) + is_script_default = true + elif(stub_target != null and stub_method != null and typeof(stub_target) != TYPE_STRING): + var method_list = null + if(typeof(stub_target) == TYPE_OBJECT and stub_target is GDScript): + method_list = stub_target.get_script_method_list() + elif(!GutUtils.is_native_class(stub_target)): + method_list = stub_target.get_method_list() + if(method_list != null): + var meta = GutUtils.find_method_meta(method_list, stub_method) + if(meta != null): + _method_meta = meta + + +func _load_defaults_from_metadata(meta): + stub_method = meta.name + var values = meta.default_args.duplicate() + while (values.size() < meta.args.size()): + values.push_front(null) + + param_defaults(values) + return_type = meta.return.type + return_val = GutConstants.get_default_return_value(meta.return.type) + + +func _get_method_meta(): + if(_method_meta == {} and typeof(stub_target) == TYPE_OBJECT): + var found_meta = GutUtils.get_method_meta(stub_target, stub_method) + if(found_meta != null): + _method_meta = found_meta + return _method_meta + + +func _error_if_locked(): + if(locked): + push_error("Cannot change stub as it has been locked.") + return true + else: + return false + +func _is_return_value_valid(val): + var is_valid = true + var meta = _get_method_meta() + if((meta.return.type != 0 or (meta.return.type == 0 and !(meta.return.usage && PROPERTY_USAGE_NIL_IS_VARIANT))) and \ + meta.return.type != typeof(return_val)): + is_valid = false + return is_valid + +# ------------------------- +# Public +# ------------------------- +func validate() -> bool: + var meta = _get_method_meta() + var to_return = true + + if(stub_method != '_init' and meta != {} and call_this == null): + if(!_is_return_value_valid(return_val)): + _lgr.error(str("Method [", stub_method, "] was stubbed to return invalid value [", return_val, "].")) + to_return = false + + return to_return + + +func to_return(val): + if(_error_if_locked()): + return + return_val = val + call_super = false + _is_return_override = true + + return self + + +func to_do_nothing(): + var meta = _get_method_meta() + if(meta != {}): + to_return(GutConstants.get_default_return_value(meta.return.type)) + return self + + +func to_call_super(): + if(_error_if_locked()): + return + + call_super = true + _is_call_override = true + return self + + +func to_use_singleton(): + return to_call_super() + + +func to_call(callable : Callable): + if(_error_if_locked()): + return + + call_this = callable + _is_call_override = true + return self + + +func 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): + parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] + var idx = 0 + while(idx < parameters.size()): + if(str(parameters[idx]) == NOT_SET): + parameters.remove_at(idx) + else: + idx += 1 + return self + + +func param_count(_x): + _lgr.deprecated("Stubbing param_count is no longer required or supported.") + return self + + +func param_defaults(values): + if(_error_if_locked()): + return + + var meta = _get_method_meta() + if(meta != {} and meta.flags & METHOD_FLAG_VARARG): + _lgr.error("Cannot stub defaults for methods with varargs: " + meta.name) + else: + parameter_defaults = values + _is_defaults_override = true + return self + + +func is_default_override_only(): + return is_defaults_override() and !is_return_override() and !is_call_override() + + +func is_return_override(): + return _is_return_override + + +func is_defaults_override(): + return _is_defaults_override + + +func is_call_override(): + return _is_call_override + + +func to_s(): + var base_string = str(stub_target, '.', stub_method) + + if(parameter_defaults.size() > 0): + if(is_script_default): + base_string += " SCRIPT DEFAULTS" + else: + base_string += " STUB DEFAULTS" + base_string += str(" ", parameter_defaults) + + if(call_super): + base_string += " to call SUPER" + + if(call_this != null): + base_string += str(" to call ", call_this) + + if(parameters != null): + base_string += str(' with params (', parameters, ') returns ', return_val) + else: + base_string += str(' returns ', return_val) + + return base_string diff --git a/addons/gut/stub_params.gd.uid b/addons/gut/stub_params.gd.uid new file mode 100644 index 0000000..a5ba1a3 --- /dev/null +++ b/addons/gut/stub_params.gd.uid @@ -0,0 +1 @@ +uid://dblktyc7hyt4f diff --git a/addons/gut/stubber.gd b/addons/gut/stubber.gd new file mode 100644 index 0000000..eda7ee9 --- /dev/null +++ b/addons/gut/stubber.gd @@ -0,0 +1,204 @@ +var parameter_stubs = GutUtils.Stubs.new() +var action_stubs = GutUtils.Stubs.new() + +var _lgr = GutUtils.get_logger() +var _strutils = GutUtils.Strutils.new() +# Since StubParams can be chained, add_params does not get the completely +# configured instance. All stubs are added to this cache first, then whenever +# a retrieval is attempted the cache is flushed into parameter_stubs and +# action_stubs. This was introduced because it was easier to keep parameter +# defaults separate from action stubs. The only way to do that though is to +# parse the stubs after they have been added. +var _stub_cache = [] + + +func _flush_cache(): + for stub_params in _stub_cache: + stub_params.logger = _lgr + + if(stub_params.is_defaults_override()): + parameter_stubs.add_stub(stub_params) + + if(!stub_params.is_default_override_only()): + action_stubs.add_stub(stub_params) + + if(!stub_params.is_script_default): + stub_params.validate() + # lock the params so that any changes that would affect which bucket + # the params were put in can't be changed. + stub_params.locked = true + _stub_cache.clear() + + +# Searches returns for an entry that matches the instance or the class that +# passed in obj is. +# +# obj can be an instance, class, or a path. +func _find_action_stub(obj, method, parameters=null): + _flush_cache() + + var to_return = null + var matches = action_stubs.get_all_stubs(obj, method) + var param_match = null + var null_match = null + var default_match = null + + if(matches.size() == 0): + return null + + for i in range(matches.size()): + var cur_stub = matches[i] + if(cur_stub.is_script_default): + default_match = cur_stub + elif(cur_stub.parameters == parameters): + param_match = cur_stub + elif(cur_stub._method_meta != {} and cur_stub.parameters != null and cur_stub.parameters.size() < cur_stub._method_meta.args.size()): + var params = cur_stub.parameters + var defaults = get_parameter_defaults(obj, method) + if(params != null): + if(defaults != null): + for j in range(params.size() -1, defaults.size() - params.size()): + params.append(defaults[j + 1]) + else: + pass + # print("NO DEFAULTS for ", obj, '.', method) + if(params == cur_stub.parameters): + param_match = cur_stub + elif(cur_stub.parameters == null and !cur_stub.is_default_override_only()): + null_match = cur_stub + + if(default_match != null): + to_return = default_match + + # We have matching parameter values so return the stub value for that + if(param_match != null): + to_return = param_match + # We found a case where the parameters were not specified so return + # parameters for that. Only do this if the null match is not *just* + # a paramerter override stub. + elif(null_match != null): + to_return = null_match + + return to_return + + + +# ############## +# Public +# ############## + +func add_stub(stub_params): + if(typeof(stub_params.stub_target) == TYPE_STRING): + if(!FileAccess.file_exists(stub_params.stub_target)): + return + + _stub_cache.append(stub_params) + + +# It will use the optional list of parameter values to find a value. If +# the object was stubbed with no parameters than any parameters will match. +# If it was stubbed with specific parameter values then it will try to match. +# If the parameters do not match BUT there was also an empty parameter list stub +# then it will return those. +# If it cannot find anything that matches then null is returned. +# +# Parameters +# obj: this should be an instance of a doubled object. +# method: the method name +# parameters: optional array of parameter vales to find a return value for. +func get_return(obj, method, parameters=null): + var stub_info = _find_action_stub(obj, method, parameters) + if(stub_info != null): + return stub_info.return_val + else: + var default = parameter_stubs.get_default_stub(obj, method) + var to_return = null + if(default != null): + # This ensures that the values are unique and that any changes made + # to them in a test are not propigated to future calls of the same + # method. + to_return = GutConstants.get_default_return_value(default.return_type) + _lgr.info(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. [', to_return, '] was returned.')) + return to_return + + +func should_call_super(obj, method, parameters=null): + var stub_info = _find_action_stub(obj, method, parameters) + + var is_partial = false + if(typeof(obj) != TYPE_STRING): # some stubber tests test with strings + is_partial = obj.__gutdbl.is_partial + var should = is_partial + + if(stub_info != null): + should = stub_info.call_super + elif(!is_partial): + # this log message is here because of how the generated doubled scripts + # are structured. With this log msg here, you will only see one + # "unstubbed" info instead of multiple. + _lgr.info('Unstubbed call to ' + method + '::' + _strutils.type2str(obj)) + should = false + + return should + + +func get_call_this(obj, method, parameters=null): + var stub_info = _find_action_stub(obj, method, parameters) + + if(stub_info != null): + return stub_info.call_this + + +func get_parameter_defaults(obj, method): + _flush_cache() + var the_defaults = [] + var script_defaults = [] + var matches = parameter_stubs.get_all_stubs(obj, method) + + var i = matches.size() -1 + while(i >= 0 and the_defaults.is_empty()): + if(matches[i].is_defaults_override()): + if(matches[i].is_script_default): + script_defaults = matches[i].parameter_defaults + else: + the_defaults = matches[i].parameter_defaults + i -= 1 + + if(the_defaults.is_empty() and !script_defaults.is_empty()): + the_defaults = script_defaults + return the_defaults + + +func get_default_value(obj, method, p_index): + var the_defaults = get_parameter_defaults(obj, method) + var to_return = null + if(the_defaults != null and the_defaults.size() > p_index): + to_return = the_defaults[p_index] + return to_return + + +func clear(): + _stub_cache.clear() + if(parameter_stubs != null): + parameter_stubs.clear() + if(action_stubs != null): + action_stubs.clear() + + +func get_logger(): + return _lgr + + +func set_logger(logger): + _lgr = logger + + +func to_s(): + return str("Parameter Stubs:\n", parameter_stubs.to_s(), + "\nAction Stubs:\n" , action_stubs.to_s()) + + +func stub_defaults_from_meta(target, method_meta): + var params = GutUtils.StubParams.new(target, method_meta) + params.is_script_default = true + add_stub(params) diff --git a/addons/gut/stubber.gd.uid b/addons/gut/stubber.gd.uid new file mode 100644 index 0000000..3db8dc1 --- /dev/null +++ b/addons/gut/stubber.gd.uid @@ -0,0 +1 @@ +uid://cjrpyjnk8lkq8 diff --git a/addons/gut/stubs.gd b/addons/gut/stubs.gd new file mode 100644 index 0000000..4fe2589 --- /dev/null +++ b/addons/gut/stubs.gd @@ -0,0 +1,110 @@ +# ------------- +# { +# object(script or instance) or StringName(native class name):{ +# method_name1: [StubParams, StubParams], +# method_name2: [StubParams, StubParams] +# }, +# object(script or instance) or StringName(native class name):{ +# method_name1: [StubParams, StubParams], +# method_name2: [StubParams, StubParams] +# } +# } +var stubs = {} + +func _normalize_stub_target(target): + var to_return = null + if(GutUtils.is_native_class(target)): + to_return = GutUtils.strutils.type2str(target) + elif(typeof(target) == TYPE_OBJECT): + to_return = target + elif(typeof(target) == TYPE_STRING): + if(FileAccess.file_exists(target)): + to_return = load(target) + else: + to_return = null + return to_return + + +func _get_entries_matching_target(target): + var match_on = [target] + var trav = target + var done = false + var current = trav + + while(trav != null and !done): + if(GutUtils.is_singleton_double(trav)): + trav = trav.__gutdbl.get_singleton() + match_on.push_front(trav) + elif(GutUtils.is_instance(trav)): + trav = trav.get_script() + match_on.push_front(trav) + else: + trav = trav.get_base_script() + if(trav != null): + match_on.push_front(trav) + else: + var type_name = current.get_instance_base_type() + match_on.push_front(type_name) + trav = null + done = true + + if(trav == null): + done = true + else: + current = trav + + return match_on + + +func clear(): + stubs.clear() + + +func add_stub(stub_params): + var key = _normalize_stub_target(stub_params.stub_target) + + if(!stubs.has(key)): + stubs[key] = {} + + if(!stubs[key].has(stub_params.stub_method)): + stubs[key][stub_params.stub_method] = [] + + stubs[key][stub_params.stub_method].append(stub_params) + + +func get_all_stubs(thing, method): + var obj = _normalize_stub_target(thing) + var match_on = _get_entries_matching_target(obj) + + var matches = [] + for entry in match_on: + if(stubs.has(entry) and stubs[entry].has(method)): + matches.append_array(stubs[entry][method]) + return matches + + +func get_default_stub(thing, method): + var obj = _normalize_stub_target(thing) + var match_on = _get_entries_matching_target(obj) + var to_return = null + for entry in match_on: + if(stubs.has(entry) and stubs[entry].has(method)): + var method_stubs = stubs[entry][method] + for stub in method_stubs: + if(stub.is_script_default): + to_return = stub + return to_return + +func to_s(): + var text = '' + for thing in stubs: + text += str("-- ", thing, " --\n") + for method in stubs[thing]: + text += str("\t", method, "\n") + for i in range(stubs[thing][method].size()): + text += "\t\t" + stubs[thing][method][i].to_s() + "\n" + + if(text == ''): + text = 'No Stubs'; + + return text diff --git a/addons/gut/stubs.gd.uid b/addons/gut/stubs.gd.uid new file mode 100644 index 0000000..ae47d19 --- /dev/null +++ b/addons/gut/stubs.gd.uid @@ -0,0 +1 @@ +uid://d1rs3vqbth3td diff --git a/addons/gut/summary.gd b/addons/gut/summary.gd new file mode 100644 index 0000000..0b862c0 --- /dev/null +++ b/addons/gut/summary.gd @@ -0,0 +1,220 @@ +# ------------------------------------------------------------------------------ +# Prints things, mostly. Knows too much about gut.gd, but it's only supposed to +# work with gut.gd, so I'm fine with that. +# ------------------------------------------------------------------------------ +# a _test_collector to use when one is not provided. +var _gut = null + + +func _init(gut=null): + _gut = gut + +# --------------------- +# Private +# --------------------- +func _log_end_run_header(gut): + var lgr = gut.get_logger() + lgr.log('==============================================', lgr.fmts.yellow) + lgr.log("= Run Summary", lgr.fmts.yellow) + lgr.log('==============================================', lgr.fmts.yellow) + + +func _log_what_was_run(gut): + if(!GutUtils.is_null_or_empty(gut._select_script)): + gut.p('Ran Scripts matching "' + gut._select_script + '"') + if(!GutUtils.is_null_or_empty(gut._unit_test_name)): + gut.p('Ran Tests matching "' + gut._unit_test_name + '"') + if(!GutUtils.is_null_or_empty(gut._inner_class_name)): + gut.p('Ran Inner Classes matching "' + gut._inner_class_name + '"') + + +func _total_fmt(text, value): + var space = 18 + if(str(value) == '0'): + value = 'none' + return str(text.rpad(space), str(value).lpad(5)) + + +func _log_non_zero_total(text, value, lgr): + if(str(value) != '0'): + lgr.log(_total_fmt(text, value)) + return 1 + else: + return 0 + + +func _log_totals(gut, totals): + var lgr = gut.get_logger() + lgr.log() + + # lgr.log("---- Totals ----") + lgr.log("Totals") + lgr.log("------") + var issue_count = 0 + issue_count += _log_non_zero_total('Errors', totals.errors, lgr) + issue_count += _log_non_zero_total('Warnings', totals.warnings, lgr) + issue_count += _log_non_zero_total('Deprecated', totals.deprecated, lgr) + if(issue_count > 0): + lgr.log("") + + lgr.log(_total_fmt( 'Scripts', totals.scripts)) + lgr.log(_total_fmt( 'Tests', gut.get_test_collector().get_ran_test_count())) + lgr.log(_total_fmt( 'Passing Tests', totals.passing_tests)) + _log_non_zero_total('Failing Tests', totals.failing_tests, lgr) + _log_non_zero_total('Risky/Pending', totals.risky + totals.pending, lgr) + if(totals.failing == 0): + lgr.log(_total_fmt( 'Asserts', totals.passing + totals.failing)) + else: + lgr.log(_total_fmt( 'Asserts', str(totals.passing, '/', totals.passing + totals.failing))) + _log_non_zero_total( 'Orphans', totals.orphans, lgr) + lgr.log(_total_fmt( 'Time', str(gut.get_elapsed_time(), 's'))) + + return totals + + +func _log_nothing_run(gut): + var lgr = gut.get_logger() + lgr.error("Nothing was run.") + lgr.log('On the one hand nothing failed, on the other hand nothing did anything.') + _log_what_was_run(gut) + + +# --------------------- +# Public +# --------------------- +func log_all_non_passing_tests(gut=_gut): + var test_collector = gut.get_test_collector() + var lgr = gut.get_logger() + + var to_return = { + passing = 0, + non_passing = 0 + } + + for test_script in test_collector.scripts: + lgr.set_indent_level(0) + + if(test_script.was_skipped or test_script.get_fail_count() > 0 or test_script.get_pending_count() > 0): + lgr.log("\n" + test_script.get_full_name(), lgr.fmts.underline) + + if(test_script.was_skipped): + lgr.inc_indent() + var skip_msg = str('[Risky] Script was skipped: ', test_script.skip_reason) + lgr.log(skip_msg, lgr.fmts.yellow) + lgr.dec_indent() + + var test_fail_count = 0 + for test in test_script.tests: + if(test.was_run): + if(test.is_passing()): + to_return.passing += 1 + else: + to_return.non_passing += 1 + lgr.log(str('- ', test.name)) + lgr.inc_indent() + + for i in range(test.fail_texts.size()): + lgr.failed(test.fail_texts[i]) + test_fail_count += 1 + for i in range(test.pending_texts.size()): + lgr.pending(test.pending_texts[i]) + if(test.is_risky()): + lgr.risky('Did not assert') + lgr.dec_indent() + + if(test_script.get_fail_count() > test_fail_count): + lgr.failed("before_all/after_all assert failed") + + return to_return + + +func log_the_final_line(totals, gut): + var lgr = gut.get_logger() + var grand_total_text = "" + var grand_total_fmt = lgr.fmts.none + if(totals.failing_tests > 0): + grand_total_text = str(totals.failing_tests, " failing tests") + grand_total_fmt = lgr.fmts.red + elif(totals.failing > 0): # no failing tests, but some failing asserts + grand_total_text = str(totals.failing, " assert(s) in before_all/after_all methods failed") + grand_total_fmt = lgr.fmts.red + elif(totals.risky > 0 or totals.pending > 0): + grand_total_text = str(totals.risky + totals.pending, " pending/risky tests.") + grand_total_fmt = lgr.fmts.yellow + else: + grand_total_text = "All tests passed!" + grand_total_fmt = lgr.fmts.green + + lgr.log(str("---- ", grand_total_text, " ----"), grand_total_fmt) + + +func log_totals(gut, totals): + var lgr = gut.get_logger() + var orig_indent = lgr.get_indent_level() + lgr.set_indent_level(0) + _log_totals(gut, totals) + lgr.set_indent_level(orig_indent) + + +func get_totals(gut=_gut): + var tc = gut.get_test_collector() + var lgr = gut.get_logger() + + var totals = { + failing = 0, + failing_tests = 0, + passing = 0, + passing_tests = 0, + pending = 0, + risky = 0, + scripts = tc.get_ran_script_count(), + tests = 0, + + deprecated = lgr.get_deprecated().size(), + errors = lgr.get_errors().size(), + warnings = lgr.get_warnings().size(), + } + + for s in tc.scripts: + # assert totals + totals.passing += s.get_pass_count() + totals.pending += s.get_pending_count() + totals.failing += s.get_fail_count() + + # test totals + totals.tests += s.get_ran_test_count() + totals.passing_tests += s.get_passing_test_count() + totals.failing_tests += s.get_failing_test_count() + totals.risky += s.get_risky_count() + + totals.orphans = gut.get_orphan_counter().orphan_count() + + return totals + + +func log_version_update(gut): + var ud = GutUtils.UpdateDetector.new() + ud.check_for_update() + var recommended = ud.get_gut_version_for_godot_version() + if(recommended != GutUtils.version_numbers.gut_version): + gut.get_logger().log(ud.get_update_string()) + ud.free() + + +func log_end_run(gut=_gut): + var totals = get_totals(gut) + if(totals.tests == 0): + _log_nothing_run(gut) + return + + _log_end_run_header(gut) + var lgr = gut.get_logger() + + log_all_non_passing_tests(gut) + log_totals(gut, totals) + lgr.log("\n") + + _log_what_was_run(gut) + log_the_final_line(totals, gut) + log_version_update(gut) + lgr.log("") diff --git a/addons/gut/summary.gd.uid b/addons/gut/summary.gd.uid new file mode 100644 index 0000000..d8ccd92 --- /dev/null +++ b/addons/gut/summary.gd.uid @@ -0,0 +1 @@ +uid://byxw2c883i2kr diff --git a/addons/gut/test.gd b/addons/gut/test.gd new file mode 100644 index 0000000..fd41caa --- /dev/null +++ b/addons/gut/test.gd @@ -0,0 +1,2923 @@ +class_name GutTest +extends Node +## This is the base class for your GUT test scripts.[br] +## [br] +## GUT Wiki: [url=https://gut.readthedocs.io]https://gut.readthedocs.io[/url] +## [br] +## Simple Example +## [codeblock] +## extends GutTest +## +## func before_all(): +## gut.p("before_all called" +## +## func before_each(): +## gut.p("before_each called") +## +## func after_each(): +## gut.p("after_each called") +## +## func after_all(): +## gut.p("after_all called") +## +## func test_assert_eq_letters(): +## assert_eq("asdf", "asdf", "Should pass") +## +## func test_assert_eq_number_not_equal(): +## assert_eq(1, 2, "Should fail. 1 != 2") +## [/codeblock] + + +# Normalizes p1 and p2 into object/signal_name/signal_ref(sig). Additional +# parameters are optional and will be placed into the others array. This +# class is used in refactoring signal methods to accept a reference to the +# signal instead an object and the signal name. +class SignalAssertParameters: + var object = null + var signal_name = null + var sig = null + var others := [] + + func _init(p1, p2, p3=null, p4=null, p5=null, p6=null): + others = [p3, p4, p5, p6] + if(p1 is Signal): + object = p1.get_object() + signal_name = p1.get_name() + others.push_front(p2) + sig = p1 + else: + object = p1 + signal_name = p2 + sig = object.get(signal_name) + + +const EDITOR_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE | PROPERTY_USAGE_DEFAULT +const VARIABLE_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE +# Convenience copy of GutUtils.DOUBLE_STRATEGY +var DOUBLE_STRATEGY = GutUtils.DOUBLE_STRATEGY + +## Reference to [addons/gut/parameter_factory.gd] script. +var ParameterFactory = GutUtils.ParameterFactory +## @ignore +var CompareResult = GutUtils.CompareResult +## Reference to [GutInputFactory] class that was originally used to reference +## the Input Factory before the class_name was introduced. +var InputFactory = GutInputFactory +## Reference to [GutInputSender]. This was the way you got to the [GutInputSender] +## before it was given a [code]class_name[/code] +var InputSender = GutUtils.InputSender + +# Need a reference to the instance that is running the tests. This +# is set by the gut class when it runs the test script. +var gut: GutMain = null +# Reference to the collected_script.gd instance that was used to create this. +# This makes getting to meta data about the test easier. This is set by +# collected_script.get_new(). +var collected_script = null +var wait_log_delay = .5 : + set(val): + if(_awaiter != null): + _awaiter.await_logger.wait_log_delay = val + wait_log_delay = val +var _compare = GutUtils.Comparator.new() +var _disable_strict_datatype_checks = false +# Holds all the text for a test's fail/pass. This is used for testing purposes +# to see the text of a failed sub-test in test_test.gd +var _fail_pass_text = [] +# Summary counts for the test. +var _summary = { + asserts = 0, + passed = 0, + failed = 0, + tests = 0, + pending = 0 +} + +# This is used to watch signals so we can make assertions about them. +var _signal_watcher = load('res://addons/gut/signal_watcher.gd').new() +var _lgr = GutUtils.get_logger() +var _strutils = GutUtils.Strutils.new() +var _awaiter = null +var _was_ready_called = false + +# Used to track time/physics/idle frames during test method execution +# They are set all together in reset_start_times +var _unixtime_began_tracking := 0.0 +var _elapsed_msec_start := 0 +var _elapsed_usec_start := 0 +var _elapsed_physics_frames_start := 0 +var _elapsed_process_frames_start := 0 + +# I haven't decided if we should be using _ready or not. Right now gut.gd will +# call this if _ready was not called (because it was overridden without a super +# call). Maybe gut.gd should just call _do_ready_stuff (after we rename it to +# something better). I'm leaving all this as it is until it bothers me more. +func _do_ready_stuff(): + _awaiter = GutUtils.Awaiter.new() + _awaiter.await_logger.wait_log_delay = wait_log_delay + add_child(_awaiter) + _was_ready_called = true + + +func _ready(): + _do_ready_stuff() + + +func _notification(what): + # Tests are never expected to re-enter the tree. Tests are removed from the + # tree after they are run. + if(what == NOTIFICATION_EXIT_TREE): + # print(_strutils.type2str(self), ': exit_tree') + _awaiter.queue_free() + elif(what == NOTIFICATION_PREDELETE): + # print(_strutils.type2str(self), ': predelete') + if(is_instance_valid(_awaiter)): + _awaiter.queue_free() + + +#region Private +# ---------------- + + +func _str(thing): + return _strutils.type2str(thing) + + +func _str_precision(value, precision): + var to_return = _str(value) + var format = str('%.', precision, 'f') + if(typeof(value) == TYPE_FLOAT): + to_return = format % value + elif(typeof(value) == TYPE_VECTOR2): + to_return = str('VECTOR2(', format % value.x, ', ', format %value.y, ')') + elif(typeof(value) == TYPE_VECTOR3): + to_return = str('VECTOR3(', format % value.x, ', ', format %value.y, ', ', format % value.z, ')') + + return to_return + + +# Fail an assertion. Causes test and script to fail as well. +func _fail(text): + _summary.asserts += 1 + _summary.failed += 1 + _fail_pass_text.append('failed: ' + text) + if(gut): + _lgr.failed(gut.get_call_count_text() + text) + gut._fail(text) + + +# Pass an assertion. +func _pass(text): + _summary.asserts += 1 + _summary.passed += 1 + _fail_pass_text.append('passed: ' + text) + if(gut): + _lgr.passed(text) + gut._pass(text) + + +# Checks if the datatypes passed in match. If they do not then this will cause +# a fail to occur. If they match then TRUE is returned, FALSE if not. This is +# used in all the assertions that compare values. +func _do_datatypes_match__fail_if_not(got, expected, text): + var did_pass = true + + if(!_disable_strict_datatype_checks): + var got_type = typeof(got) + var expect_type = typeof(expected) + if(got_type != expect_type and got != null and expected != null): + # If we have a mismatch between float and int (types 2 and 3) then + # print out a warning but do not fail. + if([2, 3].has(got_type) and [2, 3].has(expect_type)): + _lgr.warn(str('Warn: Float/Int comparison. Got ', _strutils.types[got_type], + ' but expected ', _strutils.types[expect_type])) + elif([TYPE_STRING, TYPE_STRING_NAME].has(got_type) and [TYPE_STRING, TYPE_STRING_NAME].has(expect_type)): + pass + else: + _fail('Cannot compare ' + _strutils.types[got_type] + '[' + _str(got) + '] to ' + \ + _strutils.types[expect_type] + '[' + _str(expected) + ']. ' + text) + did_pass = false + + return did_pass + + +# Create a string that lists all the methods that were called on an spied +# instance. +func _get_desc_of_calls_to_instance(inst): + var BULLET = ' * ' + var calls = gut.get_spy().get_call_list_as_string(inst) + # indent all the calls + calls = BULLET + calls.replace("\n", "\n" + BULLET) + # remove_at trailing newline and bullet + calls = calls.substr(0, calls.length() - BULLET.length() - 1) + return "Calls made on " + str(inst) + "\n" + calls + + + +# Signal assertion helper. Do not call directly, use _can_make_signal_assertions +func _fail_if_does_not_have_signal(object, signal_name): + var did_fail = false + if(!_signal_watcher.does_object_have_signal(object, signal_name)): + _fail(str('Object ', object, ' does not have the signal [', signal_name, ']')) + did_fail = true + return did_fail + + +# Signal assertion helper. Do not call directly, use _can_make_signal_assertions +func _fail_if_not_watching(object): + var did_fail = false + if(!_signal_watcher.is_watching_object(object)): + _fail(str('Cannot make signal assertions because the object ', object, \ + ' is not being watched. Call watch_signals(some_object) to be able to make assertions about signals.')) + did_fail = true + return did_fail + + +# Returns text that contains original text and a list of all the signals that +# were emitted for the passed in object. +func _get_fail_msg_including_emitted_signals(text, object): + return str(text," (Signals emitted: ", _signal_watcher.get_signals_emitted(object), ")") + + +# This validates that parameters is an array and generates a specific error +# and a failure with a specific message +func _fail_if_parameters_not_array(parameters): + var invalid = parameters != null and typeof(parameters) != TYPE_ARRAY + if(invalid): + _lgr.error('The "parameters" parameter must be an array of expected parameter values.') + _fail('Cannot compare parameter values because an array was not passed.') + return invalid + + +# A bunch of common checkes used when validating a double/method pair. If +# everything is ok then an empty string is returned, otherwise the message +# is returned. +func _get_bad_method_message(inst, method_name, what_you_cant_do): + var to_return = '' + + if(!inst.has_method(method_name)): + to_return = str("You cannot ", what_you_cant_do, " [", method_name, "] because the method does not exist. ", + "This can happen if the method is virtual and not overloaded (i.e. _ready) ", + "or you have mistyped the name of the method.") + elif(GutUtils.ScriptCollector.BLACKLIST.has(method_name)): + to_return = str("Method '", method_name, "' cannot be stubbed because it ", + "is excluded by GUT when creating doubles.") + elif(!inst.__gutdbl_values.doubled_methods.has(method_name)): + to_return = str("You cannot ", what_you_cant_do, " [", method_name, "] because ", + _str(inst), ' does not overload it or it was ignored with ', + 'ignore_method_when_doubling. See Doubling ', + 'Strategy in the wiki for details on including non-overloaded ', + 'methods in a double.') + + return to_return + + +func _fail_if_not_double_or_does_not_have_method(inst, method_name): + var to_return = OK + + if(!GutUtils.is_double(inst)): + _fail(str("An instance of a Double was expected, you passed: ", _str(inst))) + to_return = ERR_INVALID_DATA + else: + var msg = _get_bad_method_message(inst, method_name, 'spy on') + if(msg != ''): + _fail(msg) + to_return = ERR_INVALID_DATA + + return to_return + + +func _create_obj_from_type(type): + var obj = null + if type.is_class("PackedScene"): + obj = type.instantiate() + add_child(obj) + else: + obj = type.new() + return obj + + +# Converts a Callabe passed through inst or inst/method_name/parameters into a +# hash so that methods that interact with Spy can accept both more easily. +func _convert_spy_args(inst, method_name, parameters): + var to_return = { + 'object':inst, + 'method_name':method_name, + 'arguments':parameters, + 'invalid_message':'ok' + } + + if(inst is Callable): + if(parameters != null): + to_return.invalid_message =\ + "3rd parameter to assert_called not supported when using a Callable." + elif(method_name != null): + to_return.invalid_message =\ + "2nd parameter to assert_called not supported when using a Callable. Bind parameter values to the callable instead." + else: + if(inst.get_bound_arguments_count() > 0): + to_return.arguments = inst.get_bound_arguments() + to_return.method_name = inst.get_method() + to_return.object = inst.get_object() + + return to_return + + +func _get_typeof_string(the_type): + var to_return = "" + if(_strutils.types.has(the_type)): + to_return += str(the_type, '(', _strutils.types[the_type], ')') + else: + to_return += str(the_type) + return to_return + + + +# Checks the object for 'get_' and 'set_' methods for the specified property. +# If found a warning is generated. +func _warn_for_public_accessors(obj, property_name): + var public_accessors = [] + var accessor_names = [ + str('get_', property_name), + str('is_', property_name), + str('set_', property_name) + ] + + for acc in accessor_names: + if(obj.has_method(acc)): + public_accessors.append(acc) + + if(public_accessors.size() > 0): + _lgr.warn (str('Public accessors ', public_accessors, ' found for property ', property_name)) + + +func _smart_double(thing, double_strat, partial): + var override_strat = GutUtils.nvl(double_strat, gut.get_doubler().get_strategy()) + var to_return = null + + if(thing is PackedScene): + if(partial): + to_return = gut.get_doubler().partial_double_scene(thing, override_strat) + else: + to_return = gut.get_doubler().double_scene(thing, override_strat) + + elif(GutUtils.is_native_class(thing)): + if(partial): + to_return = gut.get_doubler().partial_double_gdnative(thing) + else: + to_return = gut.get_doubler().double_gdnative(thing) + + elif(thing is GDScript): + if(partial): + to_return = gut.get_doubler().partial_double(thing, override_strat) + else: + to_return = gut.get_doubler().double(thing, override_strat) + + return to_return + + +# This is here to aid in the transition to the new doubling sytnax. Once this +# has been established it could be removed. We must keep the is_instance check +# going forward though. +func _are_double_parameters_valid(thing, p2, p3): + var bad_msg = "" + if(p3 != null or typeof(p2) == TYPE_STRING): + bad_msg += "Doubling using a subpath is not supported. Call register_inner_class and then pass the Inner Class to double().\n" + + if(typeof(thing) == TYPE_STRING): + bad_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" + + if(GutUtils.is_instance(thing)): + bad_msg += "double requires a script, you passed an instance: " + _str(thing) + + if(bad_msg != ""): + _lgr.error(bad_msg) + + return bad_msg == "" + +# ---------------- +#endregion +#region Virtual Methods +# ---------------- + +## Virtual Method. This is run after the script has been prepped for execution, but before `before_all` is executed. If you implement this method and return `true` or a `String` (the string is displayed in the log) then GUT will stop executing the script and mark it as risky. You might want to do this because: +## - You are porting tests from 3.x to 4.x and you don't want to comment everything out.[br] +## - Skipping tests that should not be run when in `headless` mode such as input testing that does not work in headless.[br] +## [codeblock] +## func should_skip_script(): +## if DisplayServer.get_name() == "headless": +## return "Skip Input tests when running headless" +## [/codeblock] +## - If you have tests that would normally cause the debugger to break on an error, you can skip the script if the debugger is enabled so that the run is not interrupted.[br] +## [codeblock] +## func should_skip_script(): +## return EngineDebugger.is_active() +## [/codeblock] +func should_skip_script(): + return false + + +## Virtual method. Run once before anything else in the test script is run. +func before_all(): + pass + + +## Virtual method. Run before each test is executed +func before_each(): + pass + +## Virtual method. Run after each test is executed. +func after_each(): + pass + + +## Virtual method. Run after all tests have been run. +func after_all(): + pass + +# ---------------- +#endregion +#region Misc Public +# ---------------- +## Mark the current test as pending. +func pending(text=""): + _summary.pending += 1 + if(gut): + _lgr.pending(text) + gut._pending(text) + + +## Returns true if the test is passing as of the time of this call. False if not. +func is_passing(): + if(gut.get_current_test_object() != null and + !['before_all', 'after_all'].has(gut.get_current_test_object().name)): + return gut.get_current_test_object().is_passing() and \ + gut.get_current_test_object().assert_count > 0 + else: + _lgr.error('No current test object found. is_passing must be called inside a test.') + return null + + +## Returns true if the test is failing as of the time of this call. False if not. +func is_failing(): + if(gut.get_current_test_object() != null and + !['before_all', 'after_all'].has(gut.get_current_test_object().name)): + + return gut.get_current_test_object().is_failing() + else: + _lgr.error('No current test object found. is_failing must be called inside a test.') + return null + + +## Marks the test as passing. Does not override any failing asserts or calls to +## fail_test. Same as a passing assert. +func pass_test(text): + _pass(text) + + +## Marks the test as failing. Same as a failing assert. +func fail_test(text): + _fail(text) + +## @internal +func clear_signal_watcher(): + _signal_watcher.clear() + + +## Returns the current double strategy. +func get_double_strategy(): + return gut.get_doubler().get_strategy() + + +## Sets the double strategy for all tests in the script. This should usually +## be done in [method before_all]. The double strtegy can be set per +## run/script/double. See [wiki]Double-Strategy[/wiki] +func set_double_strategy(double_strategy): + gut.get_doubler().set_strategy(double_strategy) + + +## This method will cause Gut to pause before it moves on to the next test. +## This is useful for debugging, for instance if you want to investigate the +## screen or anything else after a test has finished executing. +## [br] +## Sometimes you get lazy, and you don't remove calls to +## [code skip-lint]pause_before_teardown[/code] after you are done with them. You can +## tell GUT to ignore calls to this method through the panel or +## the command line. Setting this in your `.gutconfig.json` file is recommended +## for CI/CD Pipelines. +func pause_before_teardown(): + gut.pause_before_teardown() + + +## @internal +func get_logger(): + return _lgr + +## @internal +func set_logger(logger): + _lgr = logger + + +## This must be called in order to make assertions based on signals being +## emitted. __Right now, this only supports signals that are emitted with 9 or +## less parameters.__ This can be extended but nine seemed like enough for now. +## The Godot documentation suggests that the limit is four but in my testing +## I found you can pass more. +## [br] +## This must be called in each test in which you want to make signal based +## assertions in. You can call it multiple times with different objects. +## You should not call it multiple times with the same object in the same test. +## The objects that are watched are cleared after each test (specifically right +## before `teardown` is called). Under the covers, Gut will connect to all the +## signals an object has and it will track each time they fire. You can then +## use the following asserts and methods to verify things are acting correct. +func watch_signals(object): + _signal_watcher.watch_signals(object) + + +## This will return the number of times a signal was fired. This gives you +## the freedom to make more complicated assertions if the spirit moves you. +## This will return -1 if the signal was not fired or the object was not being +## watched, or if the object does not have the signal. +## [br][br] +## Accepts either the object and the signal name or the signal. +func get_signal_emit_count(p1, p2=null): + var sp = SignalAssertParameters.new(p1, p2) + return _signal_watcher.get_emit_count(sp.object, sp.signal_name) + + +## If you need to inspect the parameters in order to make more complicate assertions, then this will give you access to +## the parameters of any watched signal. This works the same way that +## [code skip-lint]assert_signal_emitted_with_parameters[/code] does. It takes an object, signal name, and an optional +## index. If the index is not specified then the parameters from the most recent emission will be returned. If the +## object is not being watched, the signal was not fired, or the object does not have the signal then `null` will be +## returned. +## +## [br][br] +## [b]Signatures:[/b][br] +## - get_signal_parameters([param p1]:Signal, [param p2]:parameter-index (optional))[br] +## - get_signal_parameters([param p1]:object, [param p2]:signal name, [param p3]:parameter-index (optional)) [br] +## [br] +## [b]Examples:[/b] +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## +## func test_get_signal_parameters(): +## var obj = SignalObject.new() +## watch_signals(obj) +## obj.some_signal.emit(1, 2, 3) +## obj.some_signal.emit('a', 'b', 'c') +## +## # -- Passing -- +## # passes because get_signal_parameters returns the most recent emission +## # by default +## assert_eq(get_signal_parameters(obj, 'some_signal'), ['a', 'b', 'c']) +## assert_eq(get_signal_parameters(obj.some_signal), ['a', 'b', 'c']) +## +## assert_eq(get_signal_parameters(obj, 'some_signal', 0), [1, 2, 3]) +## assert_eq(get_signal_parameters(obj.some_signal, 0), [1, 2, 3]) +## +## # if the signal was not fired null is returned +## assert_null(get_signal_parameters(obj, 'other_signal')) +## # if the signal does not exist or isn't being watched null is returned +## assert_null(get_signal_parameters(obj, 'signal_dne')) +## +## # -- Failing -- +## assert_eq(get_signal_parameters(obj, 'some_signal'), [1, 2, 3]) +## assert_eq(get_signal_parameters(obj.some_signal, 0), ['a', 'b', 'c']) +## [/codeblock] +func get_signal_parameters(p1, p2=null, p3=-1): + var sp := SignalAssertParameters.new(p1, GutUtils.nvl(p2, -1), p3) + return _signal_watcher.get_signal_parameters(sp.object, sp.signal_name, sp.others[0]) + + +## Get the parameters for a method call to a doubled object. By default it will +## return the most recent call. You can optionally specify an index for which +## call you want to get the parameters for. +## +## Can be called using a Callable for the first parameter instead of specifying +## an object and method name. When you do this, the seoncd parameter is used +## as the index. +## +## Returns: +## * an array of parameter values if a call the method was found +## * null when a call to the method was not found or the index specified was +## invalid. +func get_call_parameters(object, method_name_or_index = -1, idx=-1): + var to_return = null + var index = idx + if(object is Callable): + index = method_name_or_index + method_name_or_index = null + var converted = _convert_spy_args(object, method_name_or_index, null) + + if(GutUtils.is_double(converted.object)): + to_return = gut.get_spy().get_call_parameters( + converted.object, converted.method_name, index) + else: + _lgr.error('You must pass a doulbed object to get_call_parameters.') + + return to_return + + +## Returns the call count for a method with optional paramter matching. +## +## Can be called with a Callable instead of an object, method_name, and +## parameters. Bound arguments will be used to match call arguments. +func get_call_count(object, method_name=null, parameters=null): + var converted = _convert_spy_args(object, method_name, parameters) + return gut.get_spy().call_count(converted.object, converted.method_name, converted.arguments) + + +## Simulate a number of frames by calling '_process' and '_physics_process' (if +## the methods exist) on an object and all of its descendents. The specified frame +## time, 'delta', will be passed to each simulated call. +## +## NOTE: Objects can disable their processing methods using 'set_process(false)' and +## 'set_physics_process(false)'. This is reflected in the 'Object' methods +## 'is_processing()' and 'is_physics_processing()', respectively. To make 'simulate' +## respect this status, for example if you are testing an object which toggles +## processing, pass 'check_is_processing' as 'true'. +func simulate(obj, times, delta, check_is_processing: bool = false): + gut.simulate(obj, times, delta, check_is_processing) + + +# ------------------------------------------------------------------------------ +## Replace the node at base_node.get_node(path) with with_this. All references +## to the node via $ and get_node(...) will now return with_this. with_this will +## get all the groups that the node that was replaced had. +## [br] +## The node that was replaced is queued to be freed. +## [br] +## TODO see replace_by method, this could simplify the logic here. +# ------------------------------------------------------------------------------ +func replace_node(base_node, path_or_node, with_this): + var path = path_or_node + + if(typeof(path_or_node) != TYPE_STRING): + # This will cause an engine error if it fails. It always returns a + # NodePath, even if it fails. Checking the name count is the only way + # I found to check if it found something or not (after it worked I + # didn't look any farther). + path = base_node.get_path_to(path_or_node) + if(path.get_name_count() == 0): + _lgr.error('You passed an object that base_node does not have. Cannot replace node.') + return + + if(!base_node.has_node(path)): + _lgr.error(str('Could not find node at path [', path, ']')) + return + + var to_replace = base_node.get_node(path) + var parent = to_replace.get_parent() + var replace_name = to_replace.get_name() + + parent.remove_child(to_replace) + parent.add_child(with_this) + with_this.set_name(replace_name) + with_this.set_owner(parent) + + var groups = to_replace.get_groups() + for i in range(groups.size()): + with_this.add_to_group(groups[i]) + + to_replace.queue_free() + + +## Use this as the default value for the first parameter to a test to create +## a parameterized test. See also the ParameterFactory and Parameterized Tests. +## [br][br] +## [b]Example[/b] +## [codeblock] +## func test_with_parameters(p = use_parameters([1, 2, 3])): +## [/codeblock] +func use_parameters(params): + var ph = gut.parameter_handler + if(ph == null): + ph = GutUtils.ParameterHandler.new(params) + gut.parameter_handler = ph + + # DO NOT use gut.gd's get_call_count_text here since it decrements the + # get_call_count value. This method increments the call count in its + # return statement. + var output = str('- params[', ph.get_call_count(), ']','(', ph.get_current_parameters(), ')') + gut.p(output, gut.LOG_LEVEL_TEST_AND_FAILURES) + + return ph.next_parameters() + + +## @internal +## When used as the default for a test method parameter, it will cause the test +## to be run x times. +## +## I Hacked this together to test a method that was occassionally failing due to +## timing issues. I don't think it's a great idea, but you be the judge. If +## you find a good use for it, let me know and I'll make it a legit member +## of the api. +func run_x_times(x): + var ph = gut.parameter_handler + if(ph == null): + _lgr.warn( + str("This test uses run_x_times and you really should not be ", + "using it. I don't think it's a good thing, but I did find it ", + "temporarily useful so I left it in here and didn't document it. ", + "Well, you found it, might as well open up an issue and let me ", + "know why you're doing this.")) + var params = [] + for i in range(x): + params.append(i) + + ph = GutUtils.ParameterHandler.new(params) + gut.parameter_handler = ph + return ph.next_parameters() + + +## Checks the passed in version string (x.x.x) against the engine version to see +## if the engine version is less than the expected version. If it is then the +## test is mareked as passed (for a lack of anything better to do). The result +## of the check is returned. +## [br][br] +## [b]Example[/b] +## [codeblock] +## if(skip_if_godot_version_lt('3.5.0')): +## return +## [/codeblock] +func skip_if_godot_version_lt(expected): + var should_skip = !GutUtils.is_godot_version_gte(expected) + if(should_skip): + _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is less than ', expected)) + return should_skip + + +## Checks if the passed in version matches the engine version. The passed in +## version can contain just the major, major.minor or major.minor.path. If +## the version is not the same then the test is marked as passed. The result of +## the check is returned. +## [br][br] +## [b]Example[/b] +## [codeblock] +## if(skip_if_godot_version_ne('3.4')): +## return +## [/codeblock] +func skip_if_godot_version_ne(expected): + var should_skip = !GutUtils.is_godot_version(expected) + if(should_skip): + _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is not ', expected)) + return should_skip + + +## Registers all the inner classes in a script with the doubler. This is required +## before you can double any inner class. +func register_inner_classes(base_script): + gut.get_doubler().inner_class_registry.register(base_script) + + +## Peforms a deep compare on both values, a CompareResult instnace is returned. +## The optional max_differences paramter sets the max_differences to be displayed. +func compare_deep(v1, v2, max_differences=null): + var result = _compare.deep(v1, v2) + if(max_differences != null): + result.max_differences = max_differences + return result + + +## Resets the time/frame tracking statistics for the current test method +func reset_start_times() -> void: + _unixtime_began_tracking = Time.get_unix_time_from_system() + _elapsed_msec_start = Time.get_ticks_msec() + _elapsed_usec_start = Time.get_ticks_usec() + _elapsed_physics_frames_start = Engine.get_physics_frames() + _elapsed_process_frames_start = Engine.get_process_frames() + + +## Returns the number of seconds elapsed since test method began as a float. +func get_elapsed_sec() -> float: + return Time.get_unix_time_from_system() - _unixtime_began_tracking + + +## Returns the number of milliseconds elapsed since test method began as a float. +func get_elapsed_msec() -> int: + return Time.get_ticks_msec() - _elapsed_msec_start + + +## Returns the number of microseconds elapsed since test method began as a float. +func get_elapsed_usec() -> int: + return Time.get_ticks_usec() - _elapsed_usec_start + + +## Alias for [method GutTest.wait_process_frames] +func get_elapsed_idle_frames() -> int: + return get_elapsed_process_frames() + + +## Returns the number of process/idle frames elapsed since the test method began. +func get_elapsed_process_frames() -> int: + return Engine.get_process_frames() - _elapsed_process_frames_start + + +## Returns the number of physics frames elapsed since the test method began. +func get_elapsed_physics_frames() -> int: + return Engine.get_physics_frames() - _elapsed_physics_frames_start + + +# ---------------- +#endregion +#region Asserts +# ---------------- + +## Asserts that the expected value equals the value got. +## assert got == expected and prints optional text. See [wiki]Comparing-Things[/wiki] +## for information about comparing dictionaries and arrays. +## [br] +## See also: [method assert_ne], [method assert_same], [method assert_not_same] +## [codeblock] +## var one = 1 +## var node1 = Node.new() +## var node2 = node1 +## +## # Passing +## assert_eq(one, 1, 'one should equal one') +## assert_eq('racecar', 'racecar') +## assert_eq(node2, node1) +## assert_eq([1, 2, 3], [1, 2, 3]) +## var d1_pass = {'a':1} +## var d2_pass = d1_pass +## assert_eq(d1_pass, d2_pass) +## +## # Failing +## assert_eq(1, 2) # FAIL +## assert_eq('hello', 'world') +## assert_eq(self, node1) +## assert_eq([1, 'two', 3], [1, 2, 3, 4]) +## assert_eq({'a':1}, {'a':1}) +## [/codeblock] +func assert_eq(got, expected, text=""): + + if(_do_datatypes_match__fail_if_not(got, expected, text)): + var disp = "[" + _str(got) + "] expected to equal [" + _str(expected) + "]: " + text + var result = null + + result = _compare.simple(got, expected) + + if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): + disp = str(result.summary, ' ', text) + _lgr.info('Array/Dictionary compared by value. Use assert_same to compare references. Use assert_eq_deep to see diff when failing.') + + if(result.are_equal): + _pass(disp) + else: + _fail(disp) + + +## asserts got != expected and prints optional text. See +## [wiki]Comparing-Things[/wiki] for information about comparing dictionaries +## and arrays. +##[br] +## See also: [method assert_eq], [method assert_same], [method assert_not_same] +## [codeblock] +## var two = 2 +## var node1 = Node.new() +## +## # Passing +## assert_ne(two, 1, 'Two should not equal one.') +## assert_ne('hello', 'world') +## assert_ne(self, node1) +## +## # Failing +## assert_ne(two, 2) +## assert_ne('one', 'one') +## assert_ne('2', 2) +## [/codeblock] +func assert_ne(got, not_expected, text=""): + if(_do_datatypes_match__fail_if_not(got, not_expected, text)): + var disp = "[" + _str(got) + "] expected to not equal [" + _str(not_expected) + "]: " + text + var result = null + + result = _compare.simple(got, not_expected) + + if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): + disp = str(result.summary, ' ', text) + _lgr.info('Array/Dictionary compared by value. Use assert_not_same to compare references. Use assert_ne_deep to see diff.') + + if(result.are_equal): + _fail(disp) + else: + _pass(disp) + + +## Asserts that [param got] is within the range of [param expected] +/- [param error_interval]. +## The upper and lower bounds are included in the check. Verified to work with +## integers, floats, and Vector2. Should work with anything that can be +## added/subtracted. +## +## [codeblock] +## # Passing +## assert_almost_eq(0, 1, 1, '0 within range of 1 +/- 1') +## assert_almost_eq(2, 1, 1, '2 within range of 1 +/- 1') +## assert_almost_eq(1.2, 1.0, .5, '1.2 within range of 1 +/- .5') +## assert_almost_eq(.5, 1.0, .5, '.5 within range of 1 +/- .5') +## assert_almost_eq(Vector2(.5, 1.5), Vector2(1.0, 1.0), Vector2(.5, .5)) +## assert_almost_eq(Vector2(.5, 1.5), Vector2(1.0, 1.0), Vector2(.25, .25)) +## +## # Failing +## assert_almost_eq(1, 3, 1, '1 outside range of 3 +/- 1') +## assert_almost_eq(2.6, 3.0, .2, '2.6 outside range of 3 +/- .2') +## [/codeblock] +func assert_almost_eq(got, expected, error_interval, text=''): + var disp = "[" + _str_precision(got, 20) + "] expected to equal [" + _str(expected) + "] +/- [" + str(error_interval) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): + if not _is_almost_eq(got, expected, error_interval): + _fail(disp) + else: + _pass(disp) + + +## This is the inverse of [method assert_almost_eq]. This will pass if [param got] is +## outside the range of [param not_expected] +/- [param error_interval]. +func assert_almost_ne(got, not_expected, error_interval, text=''): + var disp = "[" + _str_precision(got, 20) + "] expected to not equal [" + _str(not_expected) + "] +/- [" + str(error_interval) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, not_expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): + if _is_almost_eq(got, not_expected, error_interval): + _fail(disp) + else: + _pass(disp) + +# ------------------------------------------------------------------------------ +# Helper function compares a value against a expected and a +/- range. Compares +# all components of Vector2, Vector3, and Vector4 as well. +# ------------------------------------------------------------------------------ +func _is_almost_eq(got, expected, error_interval) -> bool: + var result = false + var upper = expected + error_interval + var lower = expected - error_interval + + if typeof(got) in [TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4]: + result = got.clamp(lower, upper) == got + else: + result = got >= (lower) and got <= (upper) + + return(result) + +## assserts got > expected +## [codeblock] +## var bigger = 5 +## var smaller = 0 +## +## # Passing +## assert_gt(bigger, smaller, 'Bigger should be greater than smaller') +## assert_gt('b', 'a') +## assert_gt('a', 'A') +## assert_gt(1.1, 1) +## +## # Failing +## assert_gt('a', 'a') +## assert_gt(1.0, 1) +## assert_gt(smaller, bigger) +## [/codeblock] +func assert_gt(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be > than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got > expected): + _pass(disp) + else: + _fail(disp) + + +## Asserts got is greater than or equal to expected. +## [codeblock] +## var bigger = 5 +## var smaller = 0 +## +## # Passing +## assert_gte(bigger, smaller, 'Bigger should be greater than or equal to smaller') +## assert_gte('b', 'a') +## assert_gte('a', 'A') +## assert_gte(1.1, 1) +## assert_gte('a', 'a') +## +## # Failing +## assert_gte(0.9, 1.0) +## assert_gte(smaller, bigger) +## [/codeblock] +func assert_gte(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be >= than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got >= expected): + _pass(disp) + else: + _fail(disp) + +## Asserts [param got] is less than [param expected] +## [codeblock] +## var bigger = 5 +## var smaller = 0 +## +## # Passing +## assert_lt(smaller, bigger, 'Smaller should be less than bigger') +## assert_lt('a', 'b') +## assert_lt(99, 100) +## +## # Failing +## assert_lt('z', 'x') +## assert_lt(-5, -5) +## [/codeblock] +func assert_lt(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be < than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got < expected): + _pass(disp) + else: + _fail(disp) + + +## Asserts got is less than or equal to expected +func assert_lte(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be <= than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got <= expected): + _pass(disp) + else: + _fail(disp) + + +## asserts that got is true. Does not assert truthiness, only boolean values +## will pass. +func assert_true(got, text=""): + if(typeof(got) == TYPE_BOOL): + if(got): + _pass(text) + else: + _fail(text) + else: + var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") + _fail(msg) + + +## Asserts that got is false. Does not assert truthiness, only boolean values +## will pass. +func assert_false(got, text=""): + if(typeof(got) == TYPE_BOOL): + if(got): + _fail(text) + else: + _pass(text) + else: + var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") + _fail(msg) + + +## Asserts value is between (inclusive) the two expected values.[br] +## got >= expect_low and <= expect_high +## [codeblock] +## # Passing +## assert_between(5, 0, 10, 'Five should be between 0 and 10') +## assert_between(10, 0, 10) +## assert_between(0, 0, 10) +## assert_between(2.25, 2, 4.0) +## +## # Failing +## assert_between('a', 'b', 'c') +## assert_between(1, 5, 10) +## [/codeblock] +func assert_between(got, expect_low, expect_high, text=""): + var disp = "[" + _str_precision(got, 20) + "] expected to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text + + if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): + if(expect_low > expect_high): + disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" + _fail(disp) + else: + if(got < expect_low or got > expect_high): + _fail(disp) + else: + _pass(disp) + + +## Asserts value is not between (exclusive) the two expected values.[br] +## asserts that got <= expect_low or got >= expect_high. +## [codeblock] +## # Passing +## assert_not_between(1, 5, 10) +## assert_not_between('a', 'b', 'd') +## assert_not_between('d', 'b', 'd') +## assert_not_between(10, 0, 10) +## assert_not_between(-2, -2, 10) +## +## # Failing +## assert_not_between(5, 0, 10, 'Five shouldnt be between 0 and 10') +## assert_not_between(0.25, -2.0, 4.0) +## [/codeblock] +func assert_not_between(got, expect_low, expect_high, text=""): + var disp = "[" + _str_precision(got, 20) + "] expected not to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text + + if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): + if(expect_low > expect_high): + disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" + _fail(disp) + else: + if(got > expect_low and got < expect_high): + _fail(disp) + else: + _pass(disp) + + +## Uses the 'has' method of the object passed in to determine if it contains +## the passed in element. +## [codeblock] +## var an_array = [1, 2, 3, 'four', 'five'] +## var a_hash = { 'one':1, 'two':2, '3':'three'} +## +## # Passing +## assert_has(an_array, 'four') # PASS +## assert_has(an_array, 2) # PASS +## # the hash's has method checks indexes not values +## assert_has(a_hash, 'one') # PASS +## assert_has(a_hash, '3') # PASS +## +## # Failing +## assert_has(an_array, 5) # FAIL +## assert_has(an_array, self) # FAIL +## assert_has(a_hash, 3) # FAIL +## assert_has(a_hash, 'three') # FAIL +## [/codeblock] +func assert_has(obj, element, text=""): + var disp = str('Expected [', _str(obj), '] to contain value: [', _str(element), ']: ', text) + if(obj.has(element)): + _pass(disp) + else: + _fail(disp) + + +## The inverse of assert_has. +func assert_does_not_have(obj, element, text=""): + var disp = str('Expected [', _str(obj), '] to NOT contain value: [', _str(element), ']: ', text) + if(obj.has(element)): + _fail(disp) + else: + _pass(disp) + + +## asserts a file exists at the specified path +## [codeblock] +## func before_each(): +## gut.file_touch('user://some_test_file') +## +## func after_each(): +## gut.file_delete('user://some_test_file') +## +## func test_assert_file_exists(): +## # Passing +## assert_file_exists('res://addons/gut/gut.gd') +## assert_file_exists('user://some_test_file') +## +## # Failing +## assert_file_exists('user://file_does_not.exist') +## assert_file_exists('res://some_dir/another_dir/file_does_not.exist') +## [/codeblock] +func assert_file_exists(file_path): + var disp = 'expected [' + file_path + '] to exist.' + if(FileAccess.file_exists(file_path)): + _pass(disp) + else: + _fail(disp) + + +## asserts a file does not exist at the specified path +## [codeblock] +## func before_each(): +## gut.file_touch('user://some_test_file') +## +## func after_each(): +## gut.file_delete('user://some_test_file') +## +## func test_assert_file_does_not_exist(): +## # Passing +## assert_file_does_not_exist('user://file_does_not.exist') +## assert_file_does_not_exist('res://some_dir/another_dir/file_does_not.exist') +## +## # Failing +## assert_file_does_not_exist('res://addons/gut/gut.gd') +## [/codeblock] +func assert_file_does_not_exist(file_path): + var disp = 'expected [' + file_path + '] to NOT exist' + if(!FileAccess.file_exists(file_path)): + _pass(disp) + else: + _fail(disp) + + +## asserts the specified file is empty +## [codeblock] +## func before_each(): +## gut.file_touch('user://some_test_file') +## +## func after_each(): +## gut.file_delete('user://some_test_file') +## +## func test_assert_file_empty(): +## # Passing +## assert_file_empty('user://some_test_file') +## +## # Failing +## assert_file_empty('res://addons/gut/gut.gd') +## [/codeblock] +func assert_file_empty(file_path): + var disp = 'expected [' + file_path + '] to be empty' + if(FileAccess.file_exists(file_path) and gut.is_file_empty(file_path)): + _pass(disp) + else: + _fail(disp) + + +## Asserts the specified file is not empty +## [codeblock] +## func before_each(): +## gut.file_touch('user://some_test_file') +## +## func after_each(): +## gut.file_delete('user://some_test_file') +## +## func test_assert_file_not_empty(): +## # Passing +## assert_file_not_empty('res://addons/gut/gut.gd') # PASS +## +## # Failing +## assert_file_not_empty('user://some_test_file') # FAIL +## [/codeblock] +func assert_file_not_empty(file_path): + var disp = 'expected [' + file_path + '] to contain data' + if(!gut.is_file_empty(file_path)): + _pass(disp) + else: + _fail(disp) + + +## Asserts that the passed in object has a method named [param method]. +func assert_has_method(obj, method, text=''): + var disp = _str(obj) + ' should have method: ' + method + if(text != ''): + disp = _str(obj) + ' ' + text + assert_true(obj.has_method(method), disp) + + +## This is meant to make testing public get/set methods for a member variable. This was originally created for early Godot 3.x setter and getter methods. See [method assert_property] for verifying Godot 4.x accessors. This makes multiple assertions to verify: +## [br] +## [li]The object has a method called [code]get_[/code][/li] +## [li]The object has a method called [code]set_[/code][/li] +## [li]The method [code]get_[/code] returns the expected default value when first called.[/li] +## [li]Once you set the property, the [code]get_[/code] returns the new value.[/li] +## [br] +func assert_accessors(obj, property, default, set_to): + var fail_count = _summary.failed + var get_func = 'get_' + property + var set_func = 'set_' + property + + if(obj.has_method('is_' + property)): + get_func = 'is_' + property + + assert_has_method(obj, get_func, 'should have getter starting with get_ or is_') + assert_has_method(obj, set_func) + # SHORT CIRCUIT + if(_summary.failed > fail_count): + return + assert_eq(obj.call(get_func), default, 'It should have the expected default value.') + obj.call(set_func, set_to) + assert_eq(obj.call(get_func), set_to, 'The set value should have been returned.') + + +# Property search helper. Used to retrieve Dictionary of specified property +# from passed object. Returns null if not found. +# If provided, property_usage constrains the type of property returned by +# passing either: +# EDITOR_PROPERTY for properties defined as: export var some_value: int +# VARIABLE_PROPERTY for properties defined as: var another_value +func _find_object_property(obj, property_name, property_usage=null): + var result = null + var found = false + var properties = obj.get_property_list() + + while !found and !properties.is_empty(): + var property = properties.pop_back() + if property['name'] == property_name: + if property_usage == null or property['usage'] == property_usage: + result = property + found = true + return result + + +## Asserts that [param obj] exports a property with the name +## [param property_name] and a type of [param type]. The [param type] must be +## one of the various Godot built-in [code]TYPE_[/code] constants. +## [codeblock] +## class ExportClass: +## export var some_number = 5 +## export(PackedScene) var some_scene +## var some_variable = 1 +## +## func test_assert_exports(): +## var obj = ExportClass.new() +## +## # Passing +## assert_exports(obj, "some_number", TYPE_INT) +## assert_exports(obj, "some_scene", TYPE_OBJECT) +## +## # Failing +## assert_exports(obj, 'some_number', TYPE_VECTOR2) +## assert_exports(obj, 'some_scene', TYPE_AABB) +## assert_exports(obj, 'some_variable', TYPE_INT) +## [/codeblock] +func assert_exports(obj, property_name, type): + var disp = 'expected %s to have editor property [%s]' % [_str(obj), property_name] + var property = _find_object_property(obj, property_name, EDITOR_PROPERTY) + if property != null: + disp += ' of type [%s]. Got type [%s].' % [_strutils.types[type], _strutils.types[property['type']]] + if property['type'] == type: + _pass(disp) + else: + _fail(disp) + else: + _fail(disp) + + +# Signal assertion helper. +# +# Verifies that the object and signal are valid for making signal assertions. +# This will fail with specific messages that indicate why they are not valid. +# This returns true/false to indicate if the object and signal are valid. +func _can_make_signal_assertions(object, signal_name): + return !(_fail_if_not_watching(object) or _fail_if_does_not_have_signal(object, signal_name)) + + +# Check if an object has any methods which are connected to the given signal. +# If so, returns true, otherwise, returns false. +func _is_connected_to_any( + signal_ref: Signal, + callback_parent: Object, +): + var connections = signal_ref.get_object().get_signal_connection_list(signal_ref.get_name()) + for conn in connections: + if(conn['callable'].get_object() == callback_parent): + return true + return false + + +# Helper return type of _get_connection_info. +# connected is true if the objects are actually connected and false otherwise. +# The remaining parameters are used to display test information. +class _ConnectionInfo: + var connected: bool = false + var signal_name: String = "" + var method_name: String = "" + var signal_object_name: String = "" + var method_object_name: String = "" + + +# Helper function to parse parameters of assert_[not_]connected +# Attempts to parse parameters as each of the four signatures +# of assert_[not_]connected and return whether or not the signal/methods(s) +# specified by the parameters are actually connected. +func _get_connection_info(p1, p2, p3, p4) -> _ConnectionInfo: + var con_info = _ConnectionInfo.new() + + if (p1 is Signal and p2 is Callable and p3 == null and p4 == null): + con_info.signal_name = p1.get_name() + con_info.method_name = p2.get_method() + con_info.signal_object_name = _str(p1.get_object()) + con_info.method_object_name = _str(p2.get_object()) + con_info.connected = p1.is_connected(p2) + elif (p1 is Signal and p2 is Object and p3 == null and p4 == null): + con_info.signal_name = p1.get_name() + con_info.signal_object_name = _str(p1.get_object()) + con_info.method_object_name = _str(p2) + con_info.connected = _is_connected_to_any(p1, p2) + elif (p1 is Object and p2 is Object and p3 is String and p4 == null): + con_info.signal_name = p3 + con_info.signal_object_name = _str(p1) + con_info.method_object_name = _str(p2) + if (p1.has_signal(p3)): + con_info.connected = _is_connected_to_any(Signal(p1, p3), p2) + else: + con_info.connected = false + elif (p1 is Object and p2 is Object and p3 is String and p4 is String): + con_info.signal_name = p3 + con_info.method_name = p4 + con_info.signal_object_name = _str(p1) + con_info.method_object_name = _str(p2) + if (p1.has_signal(p3) and p2.has_method(p4)): + con_info.connected = Signal(p1, p3).is_connected(Callable(p2, p4)) + else: + con_info.connected = false + else: + push_error("Signal connection assertion called with bad signature. Read Docstring for correct signature.") + return con_info + + +## Asserts that `signaler_obj` is connected to `connect_to_obj` on signal `signal_name`. The method that is connected is optional. If `method_name` is supplied then this will pass only if the signal is connected to the method. If it is not provided then any connection to the signal will cause a pass. +## [br][br] +## [b]Signatures:[/b][br] +## - assert_connected([param p1]:Signal, [param p2]:connected-object)[br] +## - assert_connected([param p1]:Signal, [param p2]:connected-method)[br] +## - assert_connected([param p1]:object, [param p2]:connected-object, [param p3]:signal-name, [param p4]: connected-method-name ) +## [br][br] +## [b]Examples:[/b] +## [codeblock] +## class Signaler: +## signal the_signal +## +## class Connector: +## func connect_this(): +## pass +## func other_method(): +## pass +## +## func test_assert_connected(): +## var signaler = Signaler.new() +## var connector = Connector.new() +## signaler.the_signal.connect(connector.connect_this) +## +## # Passing +## assert_connected(signaler.the_signal, connector.connect_this) +## assert_connected(signaler.the_signal, connector) +## assert_connected(signaler, connector, 'the_signal') +## assert_connected(signaler, connector, 'the_signal', 'connect_this') +## +## # Failing +## assert_connected(signaler.the_signal, connector.other_method) +## +## var foo = Connector.new() +## assert_connected(signaler, connector, 'the_signal', 'other_method') +## assert_connected(signaler, connector, 'other_signal') +## assert_connected(signaler, foo, 'the_signal') +## [/codeblock] +func assert_connected(p1, p2, p3=null, p4=null): + var conn_result = _get_connection_info(p1, p2, p3, p4) + var connected = conn_result.connected + var signal_name = conn_result.signal_name + var method_name = conn_result.method_name + var signal_object_name = conn_result.signal_object_name + var method_object_name = conn_result.method_object_name + + var method_disp = "" + if (method_name != ""): + method_disp = str(' using method: [', method_name, '] ') + else: + method_disp = str(" using method: [", p3, "] ") + var disp = str('Expected object ', signal_object_name,\ + ' to be connected to signal: [', signal_name, '] on ',\ + method_object_name, method_disp) + if (connected): + _pass(disp) + else: + _fail(disp) + + +## The inverse of [method assert_connected]. See [method assert_connected] for parameter syntax. +## [br] +## This will fail with specific messages if the target object is connected to the specified signal on the source object. +func assert_not_connected(p1, p2, p3=null, p4=null): + var conn_result = _get_connection_info(p1, p2, p3, p4) + var connected = conn_result.connected + var signal_name = conn_result.signal_name + var method_name = conn_result.method_name + var signal_object_name = conn_result.signal_object_name + var method_object_name = conn_result.method_object_name + + var method_disp = "" + if (method_name != ""): + method_disp = str(' using method: [', method_name, '] ') + else: + method_disp = str(" using method: [", p3, "] ") + var disp = str('Expected object ', signal_object_name,\ + ' to be not connected to signal: [', signal_name, '] on ',\ + method_object_name, method_disp) + if (connected): + _fail(disp) + else: + _pass(disp) + + +## Assert that the specified object emitted the named signal. You must call +## [method watch_signals] and pass it the object that you are making assertions about. +## This will fail if the object is not being watched or if the object does not +## have the specified signal. Since this will fail if the signal does not +## exist, you can often skip using [method assert_has_signal]. +## [br][br] +## [b]Signatures:[/b][br] +## - assert_signal_emitted([param p1]:Signal, [param p2]: text )[br] +## - assert_signal_emitted([param p1]:object, [param p2]:signal-name, [param p3]: text ) +## [br][br] +## [b]Examples:[/b] +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## +## func test_assert_signal_emitted(): +## var obj = SignalObject.new() +## +## watch_signals(obj) +## obj.emit_signal('some_signal') +## +## ## Passing +## assert_signal_emitted(obj, 'some_signal') +## assert_signal_emitted(obj.some_signal) +## +## ## Failing +## # Fails with specific message that the object does not have the signal +## assert_signal_emitted(obj, 'signal_does_not_exist') +## # Fails because the object passed is not being watched +## assert_signal_emitted(SignalObject.new(), 'some_signal') +## # Fails because the signal was not emitted +## assert_signal_emitted(obj, 'other_signal') +## assert_signal_emitted(obj.other_signal) +## [/codeblock] +func assert_signal_emitted(p1, p2='', p3=""): + var sp := SignalAssertParameters.new(p1, p2, p3) + var disp = str('Expected object ', _str(sp.object), ' to have emitted signal [', sp.signal_name, ']: ', sp.others[0]) + if(_can_make_signal_assertions(sp.object, sp.signal_name)): + if(_signal_watcher.did_emit(sp.object, sp.signal_name)): + _pass(disp) + else: + _fail(_get_fail_msg_including_emitted_signals(disp, sp.object)) + + +## This works opposite of `assert_signal_emitted`. This will fail if the object +## is not being watched or if the object does not have the signal. +## [br][br] +## [b]Signatures:[/b][br] +## - assert_signal_not_emitted([param p1]:Signal, [param p2]: text )[br] +## - assert_signal_not_emitted([param p1]:object, [param p2]:signal-name, [param p3]: text ) +## [br][br] +## [b]Examples:[/b] +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## func test_assert_signal_not_emitted(): +## var obj = SignalObject.new() +## +## watch_signals(obj) +## obj.emit_signal('some_signal') +## +## # Passing +## assert_signal_not_emitted(obj, 'other_signal') +## assert_signal_not_emitted(obj.other_signal) +## +## # Failing +## # Fails with specific message that the object does not have the signal +## assert_signal_not_emitted(obj, 'signal_does_not_exist') +## # Fails because the object passed is not being watched +## assert_signal_not_emitted(SignalObject.new(), 'some_signal') +## # Fails because the signal was emitted +## assert_signal_not_emitted(obj, 'some_signal') +## [/codeblock] +func assert_signal_not_emitted(p1, p2='', p3=''): + var sp := SignalAssertParameters.new(p1, p2, p3) + var disp = str('Expected object ', _str(sp.object), ' to NOT emit signal [', sp.signal_name, ']: ', sp.others[0]) + if(_can_make_signal_assertions(sp.object, sp.signal_name)): + if(_signal_watcher.did_emit(sp.object, sp.signal_name)): + _fail(disp) + else: + _pass(disp) + + +## Asserts that a signal was fired with the specified parameters. The expected +## parameters should be passed in as an array. An optional index can be passed +## when a signal has fired more than once. The default is to retrieve the most +## recent emission of the signal. +## [br] +## This will fail with specific messages if the object is not being watched or +## the object does not have the specified signal +## [br][br] +## [b]Signatures:[/b][br] +## - assert_signal_emitted_with_parameters([param p1]:Signal, [param p2]:expected-parameters, [param p3]: index )[br] +## - assert_signal_emitted_with_parameters([param p1]:object, [param p2]:signal-name, [param p3]:expected-parameters, [param p4]: index ) +## [br][br] +## [b]Examples:[/b] +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## func test_assert_signal_emitted_with_parameters(): +## var obj = SignalObject.new() +## +## watch_signals(obj) +## # emit the signal 3 times to illustrate how the index works in +## # assert_signal_emitted_with_parameters +## obj.emit_signal('some_signal', 1, 2, 3) +## obj.emit_signal('some_signal', 'a', 'b', 'c') +## obj.emit_signal('some_signal', 'one', 'two', 'three') +## +## # Passing +## # Passes b/c the default parameters to check are the last emission of +## # the signal +## assert_signal_emitted_with_parameters(obj, 'some_signal', ['one', 'two', 'three']) +## assert_signal_emitted_with_parameters(obj.some_signal, ['one', 'two', 'three']) +## +## # Passes because the parameters match the specified emission based on index. +## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 0) +## assert_signal_emitted_with_parameters(obj.some_signal, [1, 2, 3], 0) +## +## # Failing +## # Fails with specific message that the object does not have the signal +## assert_signal_emitted_with_parameters(obj, 'signal_does_not_exist', []) +## # Fails because the object passed is not being watched +## assert_signal_emitted_with_parameters(SignalObject.new(), 'some_signal', []) +## # Fails because parameters do not match latest emission +## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3]) +## # Fails because the parameters for the specified index do not match +## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 1) +## [/codeblock] +func assert_signal_emitted_with_parameters(p1, p2, p3=-1, p4=-1): + var sp := SignalAssertParameters.new(p1, p2, p3, p4) + var parameters = sp.others[0] + var index = sp.others[1] + + if(typeof(parameters) != TYPE_ARRAY): + _lgr.error("The expected parameters must be wrapped in an array, you passed: " + _str(parameters)) + _fail("Bad Parameters") + return + + var disp = str('Expected object ', _str(sp.object), ' to emit signal [', sp.signal_name, '] with parameters ', parameters, ', got ') + if(_can_make_signal_assertions(sp.object, sp.signal_name)): + if(_signal_watcher.did_emit(sp.object, sp.signal_name)): + var parms_got = _signal_watcher.get_signal_parameters(sp.object, sp.signal_name, index) + var diff_result = _compare.deep(parameters, parms_got) + if(diff_result.are_equal): + _pass(str(disp, parms_got)) + else: + _fail(str('Expected object ', _str(sp.object), ' to emit signal [', sp.signal_name, '] with parameters ', diff_result.summarize())) + else: + var text = str('Object ', sp.object, ' did not emit signal [', sp.signal_name, ']') + _fail(_get_fail_msg_including_emitted_signals(text, sp.object)) + + +## Asserts that a signal fired a specific number of times. +## [br][br] +## [b]Signatures:[/b][br] +## - assert_signal_emit_count([param p1]:Signal, [param p2]:expected-count, [param p3]: text )[br] +## - assert_signal_emit_count([param p1]:object, [param p2]:signal-name, [param p3]:expected-count, [param p4]: text ) +## [br][br] +## [b]Examples:[/b] +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## +## func test_assert_signal_emit_count(): +## var obj_a = SignalObject.new() +## var obj_b = SignalObject.new() +## +## watch_signals(obj_a) +## watch_signals(obj_b) +## +## obj_a.emit_signal('some_signal') +## obj_a.emit_signal('some_signal') +## +## obj_b.emit_signal('some_signal') +## obj_b.emit_signal('other_signal') +## +## # Passing +## assert_signal_emit_count(obj_a, 'some_signal', 2, 'passes') +## assert_signal_emit_count(obj_a.some_signal, 2, 'passes') +## +## assert_signal_emit_count(obj_a, 'other_signal', 0) +## assert_signal_emit_count(obj_a.other_signal, 0) +## +## assert_signal_emit_count(obj_b, 'other_signal', 1) +## +## # Failing +## # Fails with specific message that the object does not have the signal +## assert_signal_emit_count(obj_a, 'signal_does_not_exist', 99) +## # Fails because the object passed is not being watched +## assert_signal_emit_count(SignalObject.new(), 'some_signal', 99) +## # The following fail for obvious reasons +## assert_signal_emit_count(obj_a, 'some_signal', 0) +## assert_signal_emit_count(obj_b, 'other_signal', 283) +## [/codeblock] +func assert_signal_emit_count(p1, p2, p3=0, p4=""): + var sp := SignalAssertParameters.new(p1, p2, p3, p4) + var times = sp.others[0] + var text = sp.others[1] + + if(_can_make_signal_assertions(sp.object, sp.signal_name)): + var count = _signal_watcher.get_emit_count(sp.object, sp.signal_name) + var disp = str('Expected the signal [', sp.signal_name, '] emit count of [', count, '] to equal [', times, ']: ', text) + if(count== times): + _pass(disp) + else: + _fail(_get_fail_msg_including_emitted_signals(disp, sp.object)) + + +## Asserts the passed in object has a signal with the specified name. It +## should be noted that all the asserts that verify a signal was/wasn't emitted +## will first check that the object has the signal being asserted against. If +## it does not, a specific failure message will be given. This means you can +## usually skip the step of specifically verifying that the object has a signal +## and move on to making sure it emits the signal correctly. +## [codeblock] +## class SignalObject: +## signal some_signal +## signal other_signal +## +## func test_assert_has_signal(): +## var obj = SignalObject.new() +## +## ## Passing +## assert_has_signal(obj, 'some_signal') +## assert_has_signal(obj, 'other_signal') +## +## ## Failing +## assert_has_signal(obj, 'not_a real SIGNAL') +## assert_has_signal(obj, 'yea, this one doesnt exist either') +## # Fails because the signal is not a user signal. Node2D does have the +## # specified signal but it can't be checked this way. It could be watched +## # and asserted that it fired though. +## assert_has_signal(Node2D.new(), 'exit_tree') +## [/codeblock] +func assert_has_signal(object, signal_name, text=""): + var disp = str('Expected object ', _str(object), ' to have signal [', signal_name, ']: ', text) + if(_signal_watcher.does_object_have_signal(object, signal_name)): + _pass(disp) + else: + _fail(disp) + + +## Asserts that [param object] extends [param a_class]. object must be an instance of an +## object. It cannot be any of the built in classes like Array or Int or Float. +## [param a_class] must be a class, it can be loaded via load, a GDNative class such as +## Node or Label or anything else. +## [codeblock] +## # Passing +## assert_is(Node2D.new(), Node2D) +## assert_is(Label.new(), CanvasItem) +## assert_is(SubClass.new(), BaseClass) +## # Since this is a test script that inherits from test.gd, so +## # this passes. It's not obvious w/o seeing the whole script +## # so I'm telling you. You'll just have to trust me. +## assert_is(self, load('res://addons/gut/test.gd')) +## +## var Gut = load('res://addons/gut/gut.gd') +## var a_gut = Gut.new() +## assert_is(a_gut, Gut) +## +## # Failing +## assert_is(Node2D.new(), Node2D.new()) +## assert_is(BaseClass.new(), SubClass) +## assert_is('a', 'b') +## assert_is([], Node) +## [/codeblock] +func assert_is(object, a_class, text=''): + var disp = ''#var disp = str('Expected [', _str(object), '] to be type of [', a_class, ']: ', text) + var bad_param_2 = 'Parameter 2 must be a Class (like Node2D or Label). You passed ' + + if(typeof(object) != TYPE_OBJECT): + _fail(str('Parameter 1 must be an instance of an object. You passed: ', _str(object))) + elif(typeof(a_class) != TYPE_OBJECT): + _fail(str(bad_param_2, _str(a_class))) + else: + var a_str = _str(a_class) + disp = str('Expected [', _str(object), '] to extend [', a_str, ']: ', text) + if(!GutUtils.is_native_class(a_class) and !GutUtils.is_gdscript(a_class)): + _fail(str(bad_param_2, a_str)) + else: + if(is_instance_of(object, a_class)): + _pass(disp) + else: + _fail(disp) + + +## Asserts that [param object] is the the [param type] specified. [param type] +## should be one of the Godot [code]TYPE_[/code] constants. +## [codeblock] +## # Passing +## var c = Color(1, 1, 1, 1) +## gr.test.assert_typeof(c, TYPE_COLOR) +## assert_pass(gr.test) +## +## # Failing +## gr.test.assert_typeof('some string', TYPE_INT) +## assert_fail(gr.test) +## [/codeblock] +func assert_typeof(object, type, text=''): + var disp = str('Expected [typeof(', object, ') = ') + disp += _get_typeof_string(typeof(object)) + disp += '] to equal [' + disp += _get_typeof_string(type) + ']' + disp += '. ' + text + if(typeof(object) == type): + _pass(disp) + else: + _fail(disp) + + +## The inverse of [method assert_typeof] +func assert_not_typeof(object, type, text=''): + var disp = str('Expected [typeof(', object, ') = ') + disp += _get_typeof_string(typeof(object)) + disp += '] to not equal [' + disp += _get_typeof_string(type) + ']' + disp += '. ' + text + if(typeof(object) != type): + _pass(disp) + else: + _fail(disp) + + +## Assert that `text` contains `search`. Can perform case insensitive search +## by passing false for `match_case`. +## [codeblock] +## # Passing +## assert_string_contains('abc 123', 'a') +## assert_string_contains('abc 123', 'BC', false) +## assert_string_contains('abc 123', '3') +## +## # Failing +## assert_string_contains('abc 123', 'A') +## assert_string_contains('abc 123', 'BC') +## assert_string_contains('abc 123', '012') +## [/codeblock] +func assert_string_contains(text, search, match_case=true): + const empty_search = 'Expected text and search strings to be non-empty. You passed %s and %s.' + const non_strings = 'Expected text and search to both be strings. You passed %s and %s.' + var disp = 'Expected \'%s\' to contain \'%s\', match_case=%s' % [text, search, match_case] + if(typeof(text) != TYPE_STRING or typeof(search) != TYPE_STRING): + _fail(non_strings % [_str(text), _str(search)]) + elif(text == '' or search == ''): + _fail(empty_search % [_str(text), _str(search)]) + elif(match_case): + if(text.find(search) == -1): + _fail(disp) + else: + _pass(disp) + else: + if(text.to_lower().find(search.to_lower()) == -1): + _fail(disp) + else: + _pass(disp) + + +## Assert that text starts with search. Can perform case insensitive check +## by passing false for match_case +## [codeblock] +## # Passing +## assert_string_starts_with('abc 123', 'a') +## assert_string_starts_with('abc 123', 'ABC', false) +## assert_string_starts_with('abc 123', 'abc 123') +## +## ## Failing +## assert_string_starts_with('abc 123', 'z') +## assert_string_starts_with('abc 123', 'ABC') +## assert_string_starts_with('abc 123', 'abc 1234') +## [/codeblock] +func assert_string_starts_with(text, search, match_case=true): + var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' + var disp = 'Expected \'%s\' to start with \'%s\', match_case=%s' % [text, search, match_case] + if(text == '' or search == ''): + _fail(empty_search % [text, search]) + elif(match_case): + if(text.find(search) == 0): + _pass(disp) + else: + _fail(disp) + else: + if(text.to_lower().find(search.to_lower()) == 0): + _pass(disp) + else: + _fail(disp) + + +## Assert that [param text] ends with [param search]. Can perform case insensitive check by passing false for [param match_case] +## [codeblock] +## ## Passing +## assert_string_ends_with('abc 123', '123') +## assert_string_ends_with('abc 123', 'C 123', false) +## assert_string_ends_with('abc 123', 'abc 123') +## +## ## Failing +## assert_string_ends_with('abc 123', '1234') +## assert_string_ends_with('abc 123', 'C 123') +## assert_string_ends_with('abc 123', 'nope') +## [/codeblock] +func assert_string_ends_with(text, search, match_case=true): + var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' + var disp = 'Expected \'%s\' to end with \'%s\', match_case=%s' % [text, search, match_case] + var required_index = len(text) - len(search) + if(text == '' or search == ''): + _fail(empty_search % [text, search]) + elif(match_case): + if(text.find(search) == required_index): + _pass(disp) + else: + _fail(disp) + else: + if(text.to_lower().find(search.to_lower()) == required_index): + _pass(disp) + else: + _fail(disp) + + +# ------------------------------------------------------------------------------ +## Assert that a method was called on an instance of a doubled class. If +## parameters are supplied then the params passed in when called must match. +## +## Can be called with a Callabe instead of specifying the object, method_name, +## and parameters. The Callable's object must be a double. Bound arguments +## will be used to match calls based on values passed to the method. +## [br] +## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] +## [br][br] +## [b]Examples[/b] +## [codeblock] +## var my_double = double(Foobar).new() +## ... +## assert_called(my_double, 'foo') +## assert_called(my_double.foo) +## assert_called(my_double, 'foo', [1, 2, 3]) +## assert_called(my_double.foo.bind(1, 2, 3)) +## [/codeblock] +func assert_called(inst, method_name=null, parameters=null): + + if(_fail_if_parameters_not_array(parameters)): + return + + var converted = _convert_spy_args(inst, method_name, parameters) + if(converted.invalid_message != 'ok'): + fail_test(converted.invalid_message) + return + + var disp = str('Expected [',converted.method_name,'] to have been called on ',_str(converted.object)) + if(converted.arguments != null): + disp += str(' with parameters ', converted.arguments) + + if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): + if(gut.get_spy().was_called( + converted.object, converted.method_name, converted.arguments)): + _pass(disp) + else: + _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) + + +# ------------------------------------------------------------------------------ +## Assert that a method was not called on an instance of a doubled class. If +## parameters are specified then this will only fail if it finds a call that was +## sent matching parameters. +## +## Can be called with a Callabe instead of specifying the object, method_name, +## and parameters. The Callable's object must be a double. Bound arguments +## will be used to match calls based on values passed to the method. +## [br] +## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] +## [br][br] +## [b]Examples[/b] +## [codeblock] +## assert_not_called(my_double, 'foo') +## assert_not_called(my_double.foo) +## assert_not_called(my_double, 'foo', [1, 2, 3]) +## assert_not_called(my_double.foo.bind(1, 2, 3)) +## [/codeblock] +func assert_not_called(inst, method_name=null, parameters=null): + + if(_fail_if_parameters_not_array(parameters)): + return + + var converted = _convert_spy_args(inst, method_name, parameters) + if(converted.invalid_message != 'ok'): + fail_test(converted.invalid_message) + return + + var disp = str('Expected [', converted.method_name, '] to NOT have been called on ', _str(converted.object)) + + if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): + if(gut.get_spy().was_called( + converted.object, converted.method_name, converted.arguments)): + if(converted.arguments != null): + disp += str(' with parameters ', converted.arguments) + _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) + else: + _pass(disp) + + +## Asserts the the method of a double was called an expected number of times. +## If any arguments are bound to the callable then only calls with matching +## arguments will be counted. +## [br] +## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] +## [br][br] +## [b]Examples[/b] +## [codeblock] +## # assert foo was called on my_double 5 times +## assert_called_count(my_double.foo, 5) +## # assert foo, with parameters [1,2,3], was called on my_double 4 times. +## assert_called_count(my_double.foo.bind(1, 2, 3), 4) +## [/codeblock] +func assert_called_count(callable : Callable, expected_count : int): + var converted = _convert_spy_args(callable, null, null) + var count = gut.get_spy().call_count(converted.object, converted.method_name, converted.arguments) + + var param_text = '' + if(callable.get_bound_arguments_count() > 0): + param_text = ' with parameters ' + str(callable.get_bound_arguments()) + var disp = 'Expected [%s] on %s to be called [%s] times%s. It was called [%s] times.' + disp = disp % [converted.method_name, _str(converted.object), expected_count, param_text, count] + + + if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): + if(count == expected_count): + _pass(disp) + else: + _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) + + +## Asserts the passed in value is null +func assert_null(got, text=''): + var disp = str('Expected [', _str(got), '] to be NULL: ', text) + if(got == null): + _pass(disp) + else: + _fail(disp) + + +## Asserts the passed in value is not null. +func assert_not_null(got, text=''): + var disp = str('Expected [', _str(got), '] to be anything but NULL: ', text) + if(got == null): + _fail(disp) + else: + _pass(disp) + + +## Asserts that the passed in object has been freed. This assertion requires +## that you pass in some text in the form of a title since, if the object is +## freed, we won't have anything to convert to a string to put in the output +## statement. +## [br] +## [b]Note[/b] that this currently does not detect if a node has been queued free. +## [codeblock] +## var obj = Node.new() +## obj.free() +## test.assert_freed(obj, "New Node") +## [/codeblock] +func assert_freed(obj, title='something'): + var disp = title + if(is_instance_valid(obj)): + disp = _strutils.type2str(obj) + title + assert_true(not is_instance_valid(obj), "Expected [%s] to be freed" % disp) + + +## The inverse of [method assert_freed] +func assert_not_freed(obj, title='something'): + var disp = title + if(is_instance_valid(obj)): + disp = _strutils.type2str(obj) + title + assert_true(is_instance_valid(obj), "Expected [%s] to not be freed" % disp) + + +## This method will assert that no orphaned nodes have been introduced by the +## test when the assert is executed. See the [wiki]Memory-Management[/wiki] +## page for more information. +func assert_no_new_orphans(text=''): + var orphan_ids = gut.get_current_test_orphans() + var count = orphan_ids.size() + var msg = '' + if(text != ''): + msg = ': ' + text + + if(count > 0): + msg += str("\n", _strutils.indent_text(gut.get_orphan_counter().get_orphan_list_text(orphan_ids), 1, ' ')) + _fail(str('Expected no orphans, but found ', count, msg)) + else: + _pass('No new orphans found.' + msg) + + +## @ignore +func assert_set_property(obj, property_name, new_value, expected_value): + pending("this hasn't been implemented yet") + + +## @ignore +func assert_readonly_property(obj, property_name, new_value, expected_value): + pending("this hasn't been implemented yet") + + +## Assumes backing varible with be _. This will perform all the +## asserts of assert_property. Then this will set the value through the setter +## and check the backing variable value. It will then reset throught the setter +## and set the backing variable and check the getter. +func assert_property_with_backing_variable(obj, property_name, default_value, new_value, backed_by_name=null): + var setter_name = str('@', property_name, '_setter') + var getter_name = str('@', property_name, '_getter') + var backing_name = GutUtils.nvl(backed_by_name, str('_', property_name)) + var pre_fail_count = get_fail_count() + + var props = obj.get_property_list() + var found = false + var idx = 0 + while(idx < props.size() and !found): + found = props[idx].name == backing_name + idx += 1 + + assert_true(found, str(obj, ' has ', backing_name, ' variable.')) + assert_true(obj.has_method(setter_name), str('There should be a setter for ', property_name)) + assert_true(obj.has_method(getter_name), str('There should be a getter for ', property_name)) + + if(pre_fail_count == get_fail_count()): + var call_setter = Callable(obj, setter_name) + var call_getter = Callable(obj, getter_name) + + assert_eq(obj.get(backing_name), default_value, str('Variable ', backing_name, ' has default value.')) + assert_eq(call_getter.call(), default_value, 'Getter returns default value.') + call_setter.call(new_value) + assert_eq(call_getter.call(), new_value, 'Getter returns value from Setter.') + assert_eq(obj.get(backing_name), new_value, str('Variable ', backing_name, ' was set')) + + _warn_for_public_accessors(obj, property_name) + + +## This will verify that the method has a setter and getter for the property. +## It will then use the getter to check the default. Then use the +## setter with new_value and verify the getter returns the same value. +func assert_property(obj, property_name, default_value, new_value) -> void: + var pre_fail_count = get_fail_count() + + var setter_name = str('@', property_name, '_setter') + var getter_name = str('@', property_name, '_getter') + + if(typeof(obj) != TYPE_OBJECT): + _fail(str(_str(obj), ' is not an object')) + return + + assert_has_method(obj, setter_name) + assert_has_method(obj, getter_name) + + if(pre_fail_count == get_fail_count()): + var call_setter = Callable(obj, setter_name) + var call_getter = Callable(obj, getter_name) + + assert_eq(call_getter.call(), default_value, 'Default value') + call_setter.call(new_value) + assert_eq(call_getter.call(), new_value, 'Getter gets Setter value') + + _warn_for_public_accessors(obj, property_name) + + +## Performs a deep comparison between two arrays or dictionaries and asserts +## they are equal. If they are not equal then a formatted list of differences +## are displayed. See [wiki]Comparing-Things[/wiki] for more information. +func assert_eq_deep(v1, v2): + var result = compare_deep(v1, v2) + if(result.are_equal): + _pass(result.get_short_summary()) + else: + _fail(result.summary) + + +## Performs a deep comparison of two arrays or dictionaries and asserts they +## are not equal. See [wiki]Comparing-Things[/wiki] for more information. +func assert_ne_deep(v1, v2): + var result = compare_deep(v1, v2) + if(!result.are_equal): + _pass(result.get_short_summary()) + else: + _fail(result.get_short_summary()) + + +## Assert v1 and v2 are the same using [code]is_same[/code]. See @GlobalScope.is_same. +func assert_same(v1, v2, text=''): + var disp = "[" + _str(v1) + "] expected to be same as [" + _str(v2) + "]: " + text + if(is_same(v1, v2)): + _pass(disp) + else: + _fail(disp) + + +## Assert using v1 and v2 are not the same using [code]is_same[/code]. See @GlobalScope.is_same. +func assert_not_same(v1, v2, text=''): + var disp = "[" + _str(v1) + "] expected to not be same as [" + _str(v2) + "]: " + text + if(is_same(v1, v2)): + _fail(disp) + else: + _pass(disp) + + +# ---------------- +#endregion +#region Error Detection +# ---------------- +var _error_type_check_methods = { + "push_error": "is_push_error", + "engine": "is_engine_error", + "push_warning":"is_push_warning" +} + +# smells like GutTrackedError needs some more constants but I'm not ready to +# make them yet +func _is_error_of_type(err, error_type_name): + return err.call(_error_type_check_methods[error_type_name]) + + +func _assert_error_count(count, error_type_name, msg): + var consumed_count = 0 + var errors = gut.error_tracker.get_errors_for_test() + var found = [] + var disp = msg + + for err in errors: + if(_is_error_of_type(err, error_type_name)): + if(consumed_count < count): + err.handled = true + consumed_count += 1 + found.append(err) + + if(disp != ''): + disp = str(': ', disp) + else: + disp = '.' + disp = str("Expected ", count, " ", error_type_name, " errors. Got ", found.size(), disp) + if(found.size() == count): + _pass(disp) + if(!_lgr.is_type_enabled(_lgr.types.passed)): + _lgr.expected_error(msg) + else: + _fail(disp) + + +func _assert_error_text(text, error_type_name, msg): + var consumed_count = 0 + var errors = gut.error_tracker.get_errors_for_test() + var found = [] + var disp = msg + + for err in errors: + if(!err.handled and _is_error_of_type(err, error_type_name) and err.contains_text(text)): + if(consumed_count == 0): + err.handled = true + consumed_count += 1 + found.append(err) + + disp = str("Expected ", error_type_name, " error containing '", text, "'. ", msg) + if(consumed_count == 1): + _pass(disp) + if(!_lgr.is_type_enabled(_lgr.types.passed)): + _lgr.expected_error(disp) + else: + _fail(disp) + + +## Get all the errors generated by the test up to this point. Each error is an instance +## of [GutTrackedError]. Setting the [member GutTrackedError.handled] [code]handled[/code] property of +## an element in the array will prevent it from causing a test to fail. +## [br][br] +## This method allows you to inspect the details of any errors that occurred and +## decide if it's the error you are expecting or not. +## [br][br] +## [codeblock] +## func divide_them(a, b): +## return a / b +## +## func test_with_script_error(): +## divide_them('one', 44) +## push_error('this is a push error') +## var errs = get_errors() +## assert_eq(errs.size(), 2, 'expected error count') +## +## # Maybe inspect some properties of the errors here. +## +## # Mark all the errors as handled. +## for e in errs: +## e.handled = true +## [/codeblock] +## See [GutTrackedError], [wiki]Error-Tracking[/wiki]. +func get_errors()->Array: + return gut.error_tracker.get_errors_for_test() + + +## Asserts that a number of engine errors were generated by the test. +## [br][br] +## [b]Note:[/b] Each error can only be asserted against once. +## [codeblock] +## func divide_them(a, b): +## return a / b +## +## func test_asserting_engine_error_count(): +## divide_them('one', 44) +## assert_engine_error_count(1, "expecing a script error") +## +## func test_no_errors(): +## assert_engine_error_count(0, 'should be no errors here') +## +## func test_this_fails_cannot_assert_an_error_twice(): +## divide_them('one', 44) +## assert_engine_error_count(1, "expecing a script error") +## # this assert will fail because we already counted it. +## assert_engine_error_count(1, "expecing a script error") +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_engine_error_count(count:int, msg:=''): + _assert_error_count(count, "engine", msg) + + +## Asserts that a single engine error containing [param text] (case insensitive) was generated +## by the test. If the expected error is found then this assert will pass and the +## test will not fail from that engine error. +## [br][br] +## [b]Note:[/b] Each error can only be asserted against once. +## [codeblock] +## func divide_them(a, b): +## return a / b +## +## func test_asserting_engine_error_text(): +## divide_them('word', 91) +## assert_engine_error('invalid operands') +## +## func test_asserting_multipe_engine_error_texts(): +## divide_them('foo', Node) +## divide_them(1729, 0) +## assert_engine_error('Division by zero') +## assert_engine_error('invalid operands') +## +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_engine_error(text, msg=''): + var t = typeof(text) + if(t == TYPE_INT or t == TYPE_FLOAT): + _fail("Use assert_engine_error_count to assert counts. I apologize. One assert that does two different things was a bad idea.") + elif(t == TYPE_STRING): + _assert_error_text(text, 'engine', msg) + else: + _fail(str("Unexpected input: ", text)) + + +## Asserts that a number of push_errors were generated by the test. +## [br][br] +## [b]Note:[/b] Each error can only be asserted against once. +## [codeblock] +## func test_with_push_error(): +## push_error("This is an error") +## assert_push_error(1, 'This test should have caused a push_error') +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_push_error_count(count : int, msg:=''): + _assert_error_count(count, "push_error", msg) + + +## Asserts that a single push error containing [param text] (case insensitive) was generated +## by the test. If the expected error is found then this assert will pass and the +## test will not fail from that push_error. +## [br][br] +## [b]Note:[/b] Each error can only be asserted against once. +## [codeblock] +## func test_push_error_text(): +## push_error("SpecialText") +## assert_push_error("CIALtex") +## +## func test_push_error_multiple_texts(): +## push_error("Error One") +## push_error("Expception two") +## assert_push_error("one") +## assert_push_error("two") +## +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_push_error(text, msg=''): + var t = typeof(text) + if(t == TYPE_INT or t == TYPE_FLOAT): + _fail("Use assert_push_error_count to assert counts. I apologize. One assert that does two different things was a bad idea.") + elif(t == TYPE_STRING): + _assert_error_text(text, 'push_error', msg) + else: + _fail(str("Unexpected input: ", text)) + + +## Asserts that a number of push_warning were generated by the test. +## [br][br] +## [b]Note:[/b] Each warning can only be asserted against once. +## [codeblock] +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_push_warning_count(count : int, msg:=''): + _assert_error_count(count, "push_warning", msg) + + +## Asserts that a single push warning containing [param text] (case insensitive) +## was generated byt the test. +## [br][br] +## [b]Note:[/b] Each warning can only be asserted against once. +## [codeblock] +## [/codeblock] +## See [wiki]Error-Tracking[/wiki]. +func assert_push_warning(text : String, msg:=''): + _assert_error_text(text, 'push_warning', msg) + + +## Prints all detected engine errors, push_error, and push_warning that were +## generated by the test. +func print_tracked_errors(): + var errors = gut.error_tracker.get_errors_for_test() + for err in errors: + print(err.to_s()) + +# ---------------- +#endregion +#region Await Helpers +# ---------------- + + +## Use with await to wait an amount of time in seconds. The optional message +## will be printed when the await starts.[br] +## See [wiki]Awaiting[/wiki] +func wait_seconds(time, msg=''): + _awaiter.wait_seconds(time, msg) + return _awaiter.timeout + + +## Use with await to wait for a signal to be emitted or a maximum amount of +## time. Returns true if the signal was emitted, false if not.[br] +## See [wiki]Awaiting[/wiki] +func wait_for_signal(sig : Signal, max_time, msg=''): + watch_signals(sig.get_object()) + _awaiter.wait_for_signal(sig, max_time, msg) + await _awaiter.timeout + return !_awaiter.did_last_wait_timeout + + +## @deprecated +## Use wait_physics_frames or wait_process_frames +## See [wiki]Awaiting[/wiki]. +func wait_frames(frames : int, msg=''): + _lgr.deprecated("wait_frames has been replaced with wait_physics_frames which is counted in _physics_process. " + + "wait_process_frames has also been added which is counted in _process.") + return wait_physics_frames(frames, msg) + + +## This returns a signal that is emitted after [param x] physics frames have +## elpased. You can await this method directly to pause execution for [param x] +## physics frames. The frames are counted prior to _physics_process being called +## on any node (when [signal SceneTree.physics_frame] is emitted). This means the +## signal is emitted after [param x] frames and just before the x + 1 frame starts. +## [codeblock] +## await wait_physics_frames(10) +## [/codeblock] +## See [wiki]Awaiting[/wiki] +func wait_physics_frames(x :int , msg=''): + if(x <= 0): + var text = str('wait_physics_frames: frames must be > 0, you passed ', x, '. 1 frames waited.') + _lgr.error(text) + x = 1 + + _awaiter.wait_physics_frames(x, msg) + return _awaiter.timeout + + +## Alias for [method GutTest.wait_process_frames] +func wait_idle_frames(x : int, msg=''): + return wait_process_frames(x, msg) + + +## This returns a signal that is emitted after [param x] process/idle frames have +## elpased. You can await this method directly to pause execution for [param x] +## process/idle frames. The frames are counted prior to _process being called +## on any node (when [signal SceneTree.process_frame] is emitted). This means the +## signal is emitted after [param x] frames and just before the x + 1 frame starts. +## [codeblock] +## await wait_process_frames(10) +## # wait_idle_frames is an alias of wait_process_frames +## await wait_idle_frames(10) +## [/codeblock] +## See [wiki]Awaiting[/wiki] +func wait_process_frames(x : int, msg=''): + if(x <= 0): + var text = str('wait_process_frames: frames must be > 0, you passed ', x, '. 1 frames waited.') + _lgr.error(text) + x = 1 + + _awaiter.wait_process_frames(x, msg) + return _awaiter.timeout + + +## Use with await to wait for [param callable] to return the boolean value +## [code]true[/code] or a maximum amount of time. All values that are not the +## boolean value [code]true[/code] are ignored. [param callable] is called +## every [code]_physics_process[/code] tick unless an optional time between +## calls is specified.[br] +## [param p3] can be the optional message or an amount of time to wait between calls.[br] +## [param p4] is the optional message if you have specified an amount of time to +## wait between calls.[br] +## Returns [code]true[/code] if [param callable] returned true before the timeout, false if not. +##[br] +##[codeblock] +## var foo = 1 +## func test_example(): +## var foo_func = func(): +## foo += 1 +## return foo == 10 +## foo = 1 +## wait_until(foo_func, 5, 'optional message') +## # or give it a time between +## foo = 1 +## wait_until(foo_func, 5, 1, +## 'this will timeout because we call it every second and are waiting a max of 10 seconds') +## +##[/codeblock] +## See also [method wait_while][br] +## See [wiki]Awaiting[/wiki] +func wait_until(callable, max_time, p3='', p4=''): + var time_between = 0.0 + var message = p4 + if(typeof(p3) != TYPE_STRING): + time_between = p3 + else: + message = p3 + + _awaiter.wait_until(callable, max_time, time_between, message) + await _awaiter.timeout + return !_awaiter.did_last_wait_timeout + + +## This is the inverse of [method wait_until]. This will continue to wait while +## [param callable] returns the boolean value [code]true[/code]. If [b]ANY[/b] +## other value is is returned then the wait will end. +## Returns [code]true[/code] if [param callable] returned a value other than +## [code]true[/code] before the timeout, [code]false[/code] if not. +##[codeblock] +## var foo = 1 +## func test_example(): +## var foo_func = func(): +## foo += 1 +## if(foo < 10): +## return true +## else: +## return 'this is not a boolean' +## foo = 1 +## wait_while(foo_func, 5, 'optional message') +## # or give it a time between +## foo = 1 +## wait_while(foo_func, 5, 1, +## 'this will timeout because we call it every second and are waiting a max of 10 seconds') +## +##[/codeblock] +## See [wiki]Awaiting[/wiki] +func wait_while(callable, max_time, p3='', p4=''): + var time_between = 0.0 + var message = p4 + if(typeof(p3) != TYPE_STRING): + time_between = p3 + else: + message = p3 + + _awaiter.wait_while(callable, max_time, time_between, message) + await _awaiter.timeout + return !_awaiter.did_last_wait_timeout + + + +## Returns whether the last wait_* method timed out. This is always true if +## the last method was wait_xxx_frames or wait_seconds. It will be false when +## using wait_for_signal and wait_until if the timeout occurs before what +## is being waited on. The wait_* methods return this value so you should be +## able to avoid calling this directly, but you can. +func did_wait_timeout(): + return _awaiter.did_last_wait_timeout + +# ---------------- +#endregion +#region Summary Data +# ---------------- + +## @internal +func get_summary(): + return _summary + + +## Returns the number of failing asserts in this script at the time this +## method was called. Call in [method after_all] to get total count for script. +func get_fail_count(): + return _summary.failed + + +## Returns the number of passing asserts in this script at the time this method +## was called. Call in [method after_all] to get total count for script. +func get_pass_count(): + return _summary.passed + + +## Returns the number of pending tests in this script at the time this method +## was called. Call in [method after_all] to get total count for script. +func get_pending_count(): + return _summary.pending + + +## Returns the total number of asserts this script has made as of the time of +## this was called. Call in [method after_all] to get total count for script. +func get_assert_count(): + return _summary.asserts + + +# Convert the _summary dictionary into text +## @internal +func get_summary_text(): + var to_return = get_script().get_path() + "\n" + to_return += str(' ', _summary.passed, ' of ', _summary.asserts, ' passed.') + if(_summary.pending > 0): + to_return += str("\n ", _summary.pending, ' pending') + if(_summary.failed > 0): + to_return += str("\n ", _summary.failed, ' failed.') + return to_return + + +# ---------------- +#endregion +#region Double Methods +# ---------------- + + +## Create a Double of [param thing]. [param thing] should be a Class, script, +## or scene. See [wiki]Doubles[/wiki] +func double(thing, double_strat=null, not_used_anymore=null): + if(GutUtils.is_singleton(thing)): + _lgr.error(str(thing, " is an Engine Singleton. Use double_singleton to create a double of this instead.")) + return null + elif(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)): + return null + + return _smart_double(thing, double_strat, false) + + +## Create a Partial Double of [param thing]. [param thing] should be a Class, +## script, or scene. See [wiki]Partial-Doubles[/wiki] +func partial_double(thing, double_strat=null, not_used_anymore=null): + if(GutUtils.is_singleton(thing)): + _lgr.error(str(thing, " is an Engine Singleton. Use partial_double_singleton to create a double of this instead.")) + return null + elif(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)): + return null + + return _smart_double(thing, double_strat, true) + + +## Creates a psuedo-double of an Engine Singleton. These doubles wrap around +## the singleton, and do not inherit from them. These doubles do not replace +## the Engine Singleton instance. You must use a local reference to the Engine +## Singleton that the double can be injected into. +## [codeblock] +## class_name UsesTime +## var t := Time +## [/codeblock] +## [codeblock] +## extends GutTest +## func test_something(): +## var dbl_time = partial_double_singleton(Time).new() +## var inst = UsesTime.new() +## inst.t = dbl_time +## [/codeblock] +## More information can be found at [wiki]Doubling-Singletons[/wiki] +func double_singleton(singleton): + if(GutUtils.GodotSingletons.class_ref.has(singleton)): + return gut.get_doubler().double_singleton(singleton) + else: + var msg = str(singleton, " is not a known Engine Singleton. Use double to create a double of this instead. ", + "Known Singletons: \n", "\n".join(GutUtils.GodotSingletons.names)) + _lgr.error(msg) + return null + +## This creates a partial double of a singleton, where all methods are intially +## stubbed to punch through to the Engine Singleton they wrap around. +## +## See [method double_singleton] and [wiki]Doubling-Singletons[/wiki] for +## more information. +func partial_double_singleton(singleton): + if(GutUtils.GodotSingletons.class_ref.has(singleton)): + return gut.get_doubler().partial_double_singleton(singleton) + else: + var msg = str(singleton, " is not a known Engine Singleton. Use partial_double to create a double of this instead. ", + "Known Singletons: \n", "\n".join(GutUtils.GodotSingletons.names)) + _lgr.error(msg) + + return null + + + +## This was implemented to allow the doubling of classes with static methods. +## There might be other valid use cases for this method, but you should always +## try stubbing before using this method. Using +## [code]stub(my_double, 'method').to_call_super()[/code] or creating a +## [method partial_double] works for any other known scenario. You cannot stub +## or spy on methods passed to [code skip-lint]ignore_method_when_doubling[/code]. +func ignore_method_when_doubling(thing, method_name): + if(typeof(thing) == TYPE_STRING): + _lgr.error('ignore_method_when_doubling no longer supports paths to scripts or scenes. Load them and pass them instead.') + return + + var r = thing + if(thing is PackedScene): + r = GutUtils.get_scene_script_object(thing) + + gut.get_doubler().add_ignored_method(r, method_name) + + +## Stub something. See [wiki]Stubbing[/wiki] for detailed information about stubbing. +func stub(thing, p2=null, p3=null): + var method_name = p2 + var subpath = null + + if(p3 != null): + subpath = p2 + method_name = p3 + + if(GutUtils.is_instance(thing) and !GutUtils.is_double(thing)): + _lgr.error(str("An instance of a Double was expected, you passed: ", _str(thing))) + return GutUtils.StubParams.new() + + var sp = null + if(typeof(thing) == TYPE_CALLABLE): + if(p2 != null or p3 != null): + _lgr.error("Only one parameter expected when using a callable.") + sp = GutUtils.StubParams.new(thing) + else: + sp = GutUtils.StubParams.new(thing, method_name, subpath) + + if(GutUtils.is_instance(sp.stub_target)): + var msg = _get_bad_method_message(sp.stub_target, sp.stub_method, 'stub') + if(msg != ''): + _lgr.error(msg) + return GutUtils.StubParams.new() + + sp.logger = _lgr + gut.get_stubber().add_stub(sp) + return sp + + +# ---------------- +#endregion +#region Memory Mgmt +# ---------------- + + +## Marks whatever is passed in to be freed after the test finishes. It also +## returns what is passed in so you can save a line of code. +## var thing = autofree(Thing.new()) +func autofree(thing): + gut.get_autofree().add_free(thing) + return thing + + +## Works the same as autofree except queue_free will be called on the object +## instead. This also imparts a brief pause after the test finishes so that +## the queued object has time to free. +func autoqfree(thing): + gut.get_autofree().add_queue_free(thing) + return thing + + +## The same as autofree but it also adds the object as a child of the test. +func add_child_autofree(node, legible_unique_name = false): + gut.get_autofree().add_free(node) + # Explicitly calling super here b/c add_child MIGHT change and I don't want + # a bug sneaking its way in here. + super.add_child(node, legible_unique_name) + return node + + +## The same as autoqfree but it also adds the object as a child of the test. +func add_child_autoqfree(node, legible_unique_name=false): + gut.get_autofree().add_queue_free(node) + # Explicitly calling super here b/c add_child MIGHT change and I don't want + # a bug sneaking its way in here. + super.add_child(node, legible_unique_name) + return node + + +# ---------------- +#endregion +#region Deprecated/Removed +# ---------------- + + +## REMOVED +## @ignore +func compare_shallow(v1, v2, max_differences=null): + _fail('compare_shallow has been removed. Use compare_deep or just compare using == instead.') + _lgr.error('compare_shallow has been removed. Use compare_deep or just compare using == instead.') + return null + + +## REMOVED +## @ignore +func assert_eq_shallow(v1, v2): + _fail('assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep') + + +## REMOVED +## @ignore +func assert_ne_shallow(v1, v2): + _fail('assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep') + + +## @deprecated: use wait_seconds +func yield_for(time, msg=''): + _lgr.deprecated('yield_for', 'wait_seconds') + return wait_seconds(time, msg) + + +## @deprecated: use wait_for_signal +func yield_to(obj, signal_name, max_wait, msg=''): + _lgr.deprecated('yield_to', 'wait_for_signal') + return await wait_for_signal(Signal(obj, signal_name), max_wait, msg) + + +## @deprecated: use wait_frames +func yield_frames(frames, msg=''): + _lgr.deprecated("yield_frames", "wait_frames") + return wait_frames(frames, msg) + + +## @deprecated: no longer supported. Use double +func double_scene(path, strategy=null): + _lgr.deprecated('test.double_scene has been removed.', 'double') + return null + + +## @deprecated: no longer supported. Use double +func double_script(path, strategy=null): + _lgr.deprecated('test.double_script has been removed.', 'double') + return null + + # var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy()) + # return gut.get_doubler().double(path, override_strat) + + +## @deprecated: no longer supported. Use register_inner_classes + double +func double_inner(path, subpath, strategy=null): + _lgr.deprecated('double_inner should not be used. Use register_inner_classes and double instead.', 'double') + return null + + var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy()) + return gut.get_doubler().double_inner(path, subpath, override_strat) + + +## @deprecated: Use [method assert_called_count] instead. +func assert_call_count(inst, method_name, expected_count, parameters=null): + gut.logger.deprecated('This has been replaced with assert_called_count which accepts a Callable with optional bound arguments.') + var callable = Callable.create(inst, method_name) + if(parameters != null): + callable = callable.bindv(parameters) + assert_called_count(callable, expected_count) + + +## @deprecated: no longer supported. +func assert_setget( + instance, name_property, + const_or_setter = null, getter="__not_set__"): + _lgr.deprecated('assert_property') + _fail('assert_setget has been removed. Use assert_property, assert_set_property, assert_readonly_property instead.') + + +# ---------------- +#endregion + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## +# View readme for usage details. +# +# Version - see gut.gd +# ############################################################################## +# Class that all test scripts must extend.` +# +# This provides all the asserts and other testing features. Test scripts are +# run by the Gut class in gut.gd +# ############################################################################## diff --git a/addons/gut/test.gd.uid b/addons/gut/test.gd.uid new file mode 100644 index 0000000..222586f --- /dev/null +++ b/addons/gut/test.gd.uid @@ -0,0 +1 @@ +uid://cnup2nbj45r4s diff --git a/addons/gut/test_collector.gd b/addons/gut/test_collector.gd new file mode 100644 index 0000000..c934c3e --- /dev/null +++ b/addons/gut/test_collector.gd @@ -0,0 +1,277 @@ +# ------------------------------------------------------------------------------ +# This class handles calling out to the test parser and maintaining an array of +# collected_script.gd. This is used for both calling the tests and tracking +# the results of each script and test's execution. +# +# This also handles exporting and importing tests. +# ------------------------------------------------------------------------------ +var CollectedScript = GutUtils.CollectedScript +var CollectedTest = GutUtils.CollectedTest + +var _test_prefix = 'test_' +var _test_class_prefix = 'Test' + +var _lgr = GutUtils.get_logger() + + +# Array of CollectedScripts. +var scripts = [] + + +func _does_inherit_from_test(thing): + var base_script = thing.get_base_script() + var to_return = false + if(base_script != null): + var base_path = base_script.get_path() + if(base_path == 'res://addons/gut/test.gd'): + to_return = true + else: + to_return = _does_inherit_from_test(base_script) + return to_return + + +func _populate_tests(test_script): + var script = test_script.load_script() + if(script == null): + print(' !!! ', test_script.path, ' could not be loaded') + return false + + test_script.is_loaded = true + var methods = script.get_script_method_list() + for i in range(methods.size()): + var name = methods[i]['name'] + if(name.begins_with(_test_prefix)): + var t = CollectedTest.new() + t.name = name + t.arg_count = methods[i]['args'].size() + test_script.tests.append(t) + t.collected_script = weakref(test_script) + + +func _get_inner_test_class_names(loaded): + var inner_classes = [] + var const_map = loaded.get_script_constant_map() + for key in const_map: + var thing = const_map[key] + if(GutUtils.is_gdscript(thing)): + if(key.begins_with(_test_class_prefix)): + if(_does_inherit_from_test(thing)): + inner_classes.append(key) + else: + _lgr.warn(str('Ignoring Inner Class ', key, + ' because it does not extend GutTest')) + + # This could go deeper and find inner classes within inner classes + # but requires more experimentation. Right now I'm keeping it at + # one level since that is what the previous version did and there + # has been no demand for deeper nesting. + # _populate_inner_test_classes(thing) + return inner_classes + + +func _parse_script(test_script): + var inner_classes = [] + var scripts_found = [] + + var loaded = GutUtils.WarningsManager.load_script_using_custom_warnings( + test_script.path, + GutUtils.warnings_when_loading_test_scripts) + + GutUtils.inner_class_registry.register(loaded) + if(_does_inherit_from_test(loaded)): + _populate_tests(test_script) + scripts_found.append(test_script.path) + inner_classes = _get_inner_test_class_names(loaded) + else: + return [] + + for i in range(inner_classes.size()): + var loaded_inner = loaded.get(inner_classes[i]) + if(_does_inherit_from_test(loaded_inner)): + var ts = CollectedScript.new(_lgr) + ts.path = test_script.path + ts.inner_class_name = inner_classes[i] + _populate_tests(ts) + scripts.append(ts) + scripts_found.append(test_script.path + '[' + inner_classes[i] +']') + + return scripts_found + + +# ----------------- +# Public +# ----------------- +func add_script(path): + # SHORTCIRCUIT + if(has_script(path)): + return [] + + # SHORTCIRCUIT + if(!FileAccess.file_exists(path)): + # This check was added so tests could create dynmaic scripts and add + # them to be run through gut. This helps cut down on creating test + # scripts to be used in test/resources. + if(ResourceLoader.has_cached(path)): + _lgr.debug("Using cached version of " + path) + else: + _lgr.error('Could not find script: ' + path) + return + + var ts = CollectedScript.new(_lgr) + ts.path = path + # Append right away because if we don't test_doubler.gd.TestInitParameters + # will HARD crash. I couldn't figure out what was causing the issue but + # appending right away, and then removing if it's not valid seems to fix + # things. It might have to do with the ordering of the test classes in + # the test collecter. I'm not really sure. + scripts.append(ts) + var parse_results = _parse_script(ts) + + if(parse_results.find(path) == -1): + _lgr.warn(str('Ignoring script ', path, ' because it does not extend GutTest')) + scripts.remove_at(scripts.find(ts)) + + return parse_results + + +func clear(): + scripts.clear() + + +func has_script(path): + var found = false + var idx = 0 + while(idx < scripts.size() and !found): + if(scripts[idx].get_full_name() == path): + found = true + else: + idx += 1 + return found + + +func export_tests(path): + var success = true + var f = ConfigFile.new() + for i in range(scripts.size()): + scripts[i].export_to(f, str('CollectedScript-', i)) + var result = f.save(path) + if(result != OK): + _lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result)) + success = false + return success + + +func import_tests(path): + var success = false + var f = ConfigFile.new() + var result = f.load(path) + if(result != OK): + _lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result)) + else: + var sections = f.get_sections() + for key in sections: + var ts = CollectedScript.new(_lgr) + ts.import_from(f, key) + _populate_tests(ts) + scripts.append(ts) + success = true + return success + + +func get_script_named(name): + return GutUtils.search_array(scripts, 'get_filename_and_inner', name) + + +func get_test_named(script_name, test_name): + var s = get_script_named(script_name) + if(s != null): + return s.get_test_named(test_name) + else: + return null + + +func to_s(): + var to_return = '' + for i in range(scripts.size()): + to_return += scripts[i].to_s() + "\n" + return to_return + +# --------------------- +# Accessors +# --------------------- +func get_logger(): + return _lgr + + +func set_logger(logger): + _lgr = logger + + +func get_test_prefix(): + return _test_prefix + + +func set_test_prefix(test_prefix): + _test_prefix = test_prefix + + +func get_test_class_prefix(): + return _test_class_prefix + + +func set_test_class_prefix(test_class_prefix): + _test_class_prefix = test_class_prefix + + +func get_scripts(): + return scripts + + +func get_ran_test_count(): + var count = 0 + for s in scripts: + count += s.get_ran_test_count() + return count + + +func get_ran_script_count(): + var count = 0 + for s in scripts: + if(s.was_run): + count += 1 + return count + +func get_test_count(): + var count = 0 + for s in scripts: + count += s.tests.size() + return count + + +func get_assert_count(): + var count = 0 + for s in scripts: + count += s.get_assert_count() + return count + + +func get_pass_count(): + var count = 0 + for s in scripts: + count += s.get_pass_count() + return count + + +func get_fail_count(): + var count = 0 + for s in scripts: + count += s.get_fail_count() + return count + + +func get_pending_count(): + var count = 0 + for s in scripts: + count += s.get_pending_count() + return count + diff --git a/addons/gut/test_collector.gd.uid b/addons/gut/test_collector.gd.uid new file mode 100644 index 0000000..8f83309 --- /dev/null +++ b/addons/gut/test_collector.gd.uid @@ -0,0 +1 @@ +uid://cly8ws3u71jk5 diff --git a/addons/gut/thing_counter.gd b/addons/gut/thing_counter.gd new file mode 100644 index 0000000..40e71ef --- /dev/null +++ b/addons/gut/thing_counter.gd @@ -0,0 +1,55 @@ +var things = {} + +func get_unique_count(): + return things.size() + + +func add_thing_to_count(thing): + if(!things.has(thing)): + things[thing] = 0 + + +func add(thing): + if(things.has(thing)): + things[thing] += 1 + else: + things[thing] = 1 + + +func has(thing): + return things.has(thing) + + +func count(thing): + var to_return = 0 + if(things.has(thing)): + to_return = things[thing] + return to_return + + +func sum(): + var to_return = 0 + for key in things: + to_return += things[key] + return to_return + + +func to_s(): + var to_return = "" + for key in things: + to_return += str(key, ": ", things[key], "\n") + to_return += str("sum: ", sum()) + return to_return + + +func get_max_count(): + var max_val = null + for key in things: + if(max_val == null or things[key] > max_val): + max_val = things[key] + return max_val + + +func add_array_items(array): + for i in range(array.size()): + add(array[i]) diff --git a/addons/gut/thing_counter.gd.uid b/addons/gut/thing_counter.gd.uid new file mode 100644 index 0000000..537b433 --- /dev/null +++ b/addons/gut/thing_counter.gd.uid @@ -0,0 +1 @@ +uid://8evk5cwvo2nu diff --git a/addons/gut/update_detector.gd b/addons/gut/update_detector.gd new file mode 100644 index 0000000..0fce023 --- /dev/null +++ b/addons/gut/update_detector.gd @@ -0,0 +1,346 @@ +extends Node +## ----------------------------------------------------------------------------- +## Checks to see if there is a better version of GUT available for a version of +## Godot. The data used to validate the GUT version includes lower and upper +## Godot versions +## +## There are 3 places where version information is located. +## * LOCAL_FILE_PATH: This is the version information that is packed with this +## release of GUT. It serves as a fallback if remote data is not available. +## This is most useful in detecting that GUT is being used with an earlier +## release of Godot and remote data is not available. +## * REMOTE_FILE_URL: This is where the the version information exists outside +## of the local copy of GUT. This data will be downlaoded to REMOTE_FILE_PATH +## whenever remote data is fetched. +## * REMOTE_FILE_PATH: This is where REMOTE_FILE_URL is downloaded to. When +## the file is downloaded, timetamp information is added to the data so that +## we can avoid downloading the file too often. This file is used whenever +## it exists. If it does not exist and we don't fetch it, LOCAL_FILE_PATH +## will be used. +## +## REMOTE_FILE_URL Location +## Currently this file points to a file on the main branch. This is the same +## file that will be shipped with each version of GUT. This allows the file to +## be updated and still be shipped with each version of GUT. +## ----------------------------------------------------------------------------- +var Vnt = load("res://addons/gut/version_numbers.gd").VerNumTools + + +const REMOTE_FILE_URL = "https://api.github.com/repos/bitwes/gut/contents/addons/gut/versions.json" +const LOCAL_FILE_PATH = "res://addons/gut/versions.json" +const REMOTE_FILE_PATH = "user://gut_temp_directory/versions.json" +const VERSION_ZERO = "0.0.0" + +class VersionData: + var _data := {} + var data_issues = [] + const VERSION_ZERO = "0.0.0" + + var Vnt = load("res://addons/gut/version_numbers.gd").VerNumTools + + + func is_gut_version_valid(gut_v, godot_v): + if(_data.releases.has(gut_v)): + var entry = _data.releases[gut_v] + return Vnt.is_version_gte(godot_v, entry.godot_min) and Vnt.is_version_lte(godot_v, entry.godot_max) + else: + return false + + + func parse_data(new_data): + data_issues.clear() + _data = {} + if(typeof(new_data) == TYPE_STRING): + _data = JSON.parse_string(new_data) + elif(typeof(new_data) == TYPE_DICTIONARY): + _data = new_data + + if(!_data.has('asset_library')): + data_issues.append("asset_library entry missing") + + if(_data.has('releases')): + for key in _data.releases: + var entry = _data.releases[key] + if(!entry.has('godot_min')): + data_issues.append(str(key, ' missing godot_min')) + if(!entry.has('godot_max')): + data_issues.append(str(key, ' missing godot_max')) + else: + data_issues.append('missing releases entry') + + if(_data.has('branches')): + for key in _data.branches: + var entry = _data.branches[key] + if(!entry.has('godot_min')): + data_issues.append(str(key, ' missing godot_min')) + if(!entry.has('godot_max')): + data_issues.append(str(key, ' missing godot_max')) + else: + data_issues.append('missing branches entry') + + + func parse_file(path): + if(FileAccess.file_exists(path)): + var text = GutUtils.get_file_as_text(path) + parse_data(text) + else: + _data = {} + + + func get_gut_version_for_godot_version(godot_v=null): + var to_return = VERSION_ZERO + if(is_empty()): + return to_return + + if(godot_v == null): + godot_v = GutUtils.version_numbers.make_godot_version_string() + + for key in _data.releases: + var entry = _data.releases[key] + if(Vnt.is_version_gte(godot_v, entry.godot_min) and Vnt.is_version_lte(godot_v, entry.godot_max)): + if(Vnt.is_version_gte(key, to_return)): + to_return = key + + if(to_return == VERSION_ZERO and _data.has('branches')): + for key in _data.branches: + var entry = _data.branches[key] + if(Vnt.is_version_gte(godot_v, entry.godot_min) and Vnt.is_version_lte(godot_v, entry.godot_max)): + to_return = key + + return to_return + + func is_empty(): + return _data.is_empty() + + func clear(): + _data.clear() + data_issues.clear() + + ## See comment in get_update_string + # func is_in_asset_library(gut_v): + # if(_data.has('asset_library')): + # return _data.asset_library == gut_v + # else: + # return false + + func get_data(): + return _data.duplicate(true) + + +var _http_request : HTTPRequest +var local_data : VersionData = VersionData.new() +var remote_data : VersionData = VersionData.new() + +var min_fetch_wait = 60 * 60 # 1 hour + +signal download_completed() +signal updated() + + +func _ready() -> void: + _setup_http_request(HTTPRequest.new()) + + +func _setup_http_request(request): + _http_request = request + add_child(_http_request) + _http_request.timeout = 3.0 + _http_request.request_completed.connect(self._http_request_completed) + + +func _write_remote_file(data): + data.fetch_timestamp = Time.get_unix_time_from_system() + GutUtils.write_file(REMOTE_FILE_PATH, JSON.stringify(data)) + + +func _url_formatter(url, link_text=null): + if(link_text == null): + return url + else: + return str(link_text, ': ', url) + + +#------------ +# Events +#------------ +# Called when the HTTP request is completed. +func _http_request_completed(result, response_code, headers, body): + var body_text = body.get_string_from_utf8() + + if(response_code == 200): + var json = JSON.new() + var err = json.parse(body_text) + if(err != OK): + push_error("[GUT] Invalid JSON: ", json.get_error_message(), '. ', body_text) + download_completed.emit() + return + var response = json.get_data() + + remote_data.parse_data(response) + if(remote_data.data_issues.size() == 0): + _write_remote_file(response.duplicate(true)) + else: + push_error("[GUT] Invalid version data: ", remote_data.data_issues) + remote_data.clear() + else: + var json = JSON.new() + var err = json.parse(body_text) + var response = {} + if(err == OK): + response = json.get_data() + + var msg = '' + if(response != null and response.has('message')): + msg = str(" (", response.message, ")") + push_error("[GUT] Could not get version info, response code: ", response_code, msg) + + download_completed.emit() + + +func _is_branch_version(v : String): + return v.find(".") == -1 +#------------ +# Public +#------------ +func fetch_remote_file(): + var headers : PackedStringArray = [ + "Accept: application/vnd.github.raw", + "X-GitHub-Api-Version: 2022-11-28" + ] + var error = _http_request.request(REMOTE_FILE_URL, headers) + if error != OK: + var errtxt = str("[GUT] An error occurred requesting version data: ", error, ".") + remote_data.data_issues.append(errtxt) + push_error(errtxt) + return error + + +func get_gut_version_for_godot_version(godot_v=null): + var to_return = VERSION_ZERO + if(godot_v == null): + godot_v = GutUtils.version_numbers.make_godot_version_string() + + var remote_latest = remote_data.get_gut_version_for_godot_version(godot_v) + var local_latest = local_data.get_gut_version_for_godot_version(godot_v) + + if(remote_latest == VERSION_ZERO and local_latest != VERSION_ZERO): + to_return = local_latest + elif(remote_latest != VERSION_ZERO and local_latest == VERSION_ZERO): + to_return = remote_latest + elif(_is_branch_version(remote_latest) and !_is_branch_version(local_latest)): + to_return = local_latest + elif(Vnt.is_version_gte(remote_latest, local_latest)): + to_return = remote_latest + else: + to_return = local_latest + + return to_return + + +func is_gut_version_valid(gut_v =null, godot_v=null): + if(gut_v == null): + gut_v = GutUtils.version_numbers.gut_version + godot_v = GutUtils.version_numbers.make_godot_version_string() + + if(!local_data.is_empty() and !remote_data.is_empty()): + return local_data.is_gut_version_valid(gut_v, godot_v) or \ + remote_data.is_gut_version_valid(gut_v, godot_v) + else: + return true + +## See comment in get_update_string +# func is_in_asset_library(gut_v): +# if(remote_data._data.has('asset_library')): +# return remote_data._data.asset_library == gut_v +# else: +# return false + + +func check_for_update(): + local_data.parse_file(LOCAL_FILE_PATH) + if(FileAccess.file_exists(REMOTE_FILE_PATH)): + remote_data.parse_file(REMOTE_FILE_PATH) + + updated.emit.call_deferred() + + +func check_for_update_with_fetch(force=false): + remote_data.parse_file(REMOTE_FILE_PATH) + var time_since_last_fetch = 60 * 60 * 24 * 10_000 # ten thousand days + + if(remote_data._data.has("fetch_timestamp")): + time_since_last_fetch = Time.get_unix_time_from_system() - remote_data._data.fetch_timestamp + + if(force or time_since_last_fetch > min_fetch_wait): + fetch_remote_file() + await download_completed + + check_for_update() + + +func get_update_string(url_formatter:Callable=_url_formatter): + var gut_v = GutUtils.version_numbers.gut_version + var godot_v = GutUtils.godot_version_string() + var version_info = str("GUT ", gut_v, " is the lastest version for Godot ", godot_v) + + var rec_ver = get_gut_version_for_godot_version(godot_v) + var rec_ver_link = url_formatter.call(str("https://github.com/bitwes/Gut/releases/tag/v", rec_ver), str("GUT ",rec_ver)) + + if(is_gut_version_valid(gut_v, godot_v)): + if(rec_ver != gut_v): + version_info = str(rec_ver_link, ' is now available!') + else: + if(rec_ver.find(".") == -1): + version_info = str("GUT does not have a release for this version of Godot yet, but it does have ", + "the branch '", rec_ver, "'.\n", + "Check the readme for install links/instructions: ", url_formatter.call('https://github.com/bitwes/Gut')) + else: + version_info = str('This version of GUT may not be compatible with Godot ', godot_v, '. ') + if(rec_ver == '0.0.0'): + version_info += str("No release or branch exists for this version of Godot yet. Check back soon.") + else: + version_info += str('Consider changing to ', rec_ver_link) + ## Commented this out for now, this needs to be reassessed for the new store + ## For now I'm leaving it out as I don't know what the urls look like or have + ## anything out there yet. + # if(rec_ver != gut_v and is_in_asset_library(rec_ver)): + # version_info += str("\nYou can update to ", rec_ver, " through the Asset Library.") + return version_info + + +func get_summary_string(): + var gut_v = GutUtils.version_numbers.gut_version + var godot_v = GutUtils.godot_version_string() + + return str("GUT: ", gut_v, "\n", + "Godot: ", godot_v, "\n", + "Valid: ", is_gut_version_valid(gut_v, godot_v), "\n", + "Latest: ", get_gut_version_for_godot_version(godot_v)) + + +func fetch_limit_wait_time(): + var remaining = -1 + if(remote_data.get_data().has("fetch_timestamp")): + var time_since_last_fetch = Time.get_unix_time_from_system() - remote_data._data.fetch_timestamp + return max(min_fetch_wait - time_since_last_fetch, 0.0) + else: + return -1 + + +func get_days_since_last_fetch(): + var to_return = 99 + if(remote_data.get_data().has("fetch_timestamp")): + var time_since_last_fetch = Time.get_unix_time_from_system() - remote_data._data.fetch_timestamp + to_return = time_since_last_fetch / (60.0 * 60.0 * 24.0) + return to_return + + +func is_empty(): + return local_data.is_empty() and remote_data.is_empty() + + +func get_all_data(): + return { + local_data:local_data.get_data(), + remote_data:remote_data.get_data() + } diff --git a/addons/gut/update_detector.gd.uid b/addons/gut/update_detector.gd.uid new file mode 100644 index 0000000..aaacb7b --- /dev/null +++ b/addons/gut/update_detector.gd.uid @@ -0,0 +1 @@ +uid://oj7xlx2xgxhh diff --git a/addons/gut/utils.gd b/addons/gut/utils.gd new file mode 100644 index 0000000..3bea3a9 --- /dev/null +++ b/addons/gut/utils.gd @@ -0,0 +1,672 @@ +@tool +class_name GutUtils +extends Object + +# Note, these cannot change since places are checking for TYPE_INT to determine +# how to process parameters. +enum DOUBLE_STRATEGY{ + INCLUDE_NATIVE, + SCRIPT_ONLY, +} + +enum DIFF { + DEEP, + SIMPLE +} + +enum TREAT_AS { + NOTHING, + FAILURE, +} + +const GUT_METADATA = '__gutdbl' + +const TEST_STATUSES = { + NO_ASSERTS = 'no asserts', + SKIPPED = 'skipped', + NOT_RUN = 'not run', + PENDING = 'pending', + # These two got the "ed" b/c pass is a reserved word and I could not + # think of better words. + FAILED = 'fail', + PASSED = 'pass' +} + +const DOUBLE_TEMPLATES = { + FUNCTION = 'res://addons/gut/double_templates/function_template.txt', + INIT = 'res://addons/gut/double_templates/init_template.txt', + SCRIPT = 'res://addons/gut/double_templates/script_template.txt', +} + +const NOTHING := '__NOTHING__' +const NO_TEST := 'NONE' +const GUT_ERROR_TYPE = 999 + + +## This dictionary defaults to all the native classes that we cannot call new +## on. It is further populated during a run so that we only have to create +## a new instance once to get the class name string. +static var gdscript_native_class_names_by_type = { + Tween:"Tween", + CanvasItem:"CanvasItem", +} + + +static var GutScene = load('res://addons/gut/GutScene.tscn') +static var LazyLoader = load('res://addons/gut/lazy_loader.gd') +static var VersionNumbers = load("res://addons/gut/version_numbers.gd") +static var WarningsManager = load("res://addons/gut/warnings_manager.gd") +static var EditorGlobals = load("res://addons/gut/gui/editor_globals.gd") +static var RunExternallyScene = load("res://addons/gut/gui/RunExternally.tscn") + +# -------------------------------- +# Lazy loaded scripts. These scripts are lazy loaded so that they can be +# declared, but will not load when this script is loaded. This gives us a +# window at the start of a run to adjust warning levels prior to loading +# everything. +# -------------------------------- +static var AutoFree = LazyLoader.new('res://addons/gut/autofree.gd'): + get: return AutoFree.get_loaded() + set(val): pass +static var Awaiter = LazyLoader.new('res://addons/gut/awaiter.gd'): + get: return Awaiter.get_loaded() + set(val): pass +static var Comparator = LazyLoader.new('res://addons/gut/comparator.gd'): + get: return Comparator.get_loaded() + set(val): pass +static var CollectedTest = LazyLoader.new('res://addons/gut/collected_test.gd'): + get: return CollectedTest.get_loaded() + set(val): pass +static var CollectedScript = LazyLoader.new('res://addons/gut/collected_script.gd'): + get: return CollectedScript.get_loaded() + set(val): pass +static var CompareResult = LazyLoader.new('res://addons/gut/compare_result.gd'): + get: return CompareResult.get_loaded() + set(val): pass +static var DiffFormatter = LazyLoader.new("res://addons/gut/diff_formatter.gd"): + get: return DiffFormatter.get_loaded() + set(val): pass +static var DiffTool = LazyLoader.new('res://addons/gut/diff_tool.gd'): + get: return DiffTool.get_loaded() + set(val): pass +static var DoubleTools = LazyLoader.new("res://addons/gut/double_tools.gd"): + get: return DoubleTools.get_loader() + set(val): pass +static var Doubler = LazyLoader.new('res://addons/gut/doubler.gd'): + get: return Doubler.get_loaded() + set(val): pass +static var DynamicGdScript = LazyLoader.new("res://addons/gut/dynamic_gdscript.gd") : + get: return DynamicGdScript.get_loaded() + set(val): pass +static var GodotSingletons = LazyLoader.new('res://addons/gut/godot_singletons.gd') : + get: return GodotSingletons.get_loaded() + set(val): pass +static var Gut = LazyLoader.new('res://addons/gut/gut.gd'): + get: return Gut.get_loaded() + set(val): pass +static var GutConfig = LazyLoader.new('res://addons/gut/gut_config.gd'): + get: return GutConfig.get_loaded() + set(val): pass +static var GutFonts = LazyLoader.new("res://addons/gut/gut_fonts.gd"): + get: return GutFonts.get_loaded() + set(val): pass +static var HookScript = LazyLoader.new('res://addons/gut/hook_script.gd'): + get: return HookScript.get_loaded() + set(val): pass +static var InnerClassRegistry = LazyLoader.new('res://addons/gut/inner_class_registry.gd'): + get: return InnerClassRegistry.get_loaded() + set(val): pass +static var InputFactory = LazyLoader.new("res://addons/gut/input_factory.gd"): + get: return InputFactory.get_loaded() + set(val): pass +static var InputSender = LazyLoader.new("res://addons/gut/input_sender.gd"): + get: return InputSender.get_loaded() + set(val): pass +static var JunitXmlExport = LazyLoader.new('res://addons/gut/junit_xml_export.gd'): + get: return JunitXmlExport.get_loaded() + set(val): pass +static var GutLogger = LazyLoader.new('res://addons/gut/logger.gd') : # everything should use get_logger + get: return GutLogger.get_loaded() + set(val): pass +static var MethodMaker = LazyLoader.new('res://addons/gut/method_maker.gd'): + get: return MethodMaker.get_loaded() + set(val): pass +static var OneToMany = LazyLoader.new('res://addons/gut/one_to_many.gd'): + get: return OneToMany.get_loaded() + set(val): pass +static var OptionMaker = LazyLoader.new('res://addons/gut/gui/option_maker.gd'): + get: return OptionMaker.get_loaded() + set(val): pass +static var OrphanCounter = LazyLoader.new('res://addons/gut/orphan_counter.gd'): + get: return OrphanCounter.get_loaded() + set(val): pass +static var ParameterFactory = LazyLoader.new('res://addons/gut/parameter_factory.gd'): + get: return ParameterFactory.get_loaded() + set(val): pass +static var ParameterHandler = LazyLoader.new('res://addons/gut/parameter_handler.gd'): + get: return ParameterHandler.get_loaded() + set(val): pass +static var Printers = LazyLoader.new('res://addons/gut/printers.gd'): + get: return Printers.get_loaded() + set(val): pass +static var ResultExporter = LazyLoader.new('res://addons/gut/result_exporter.gd'): + get: return ResultExporter.get_loaded() + set(val): pass +static var ScriptCollector = LazyLoader.new('res://addons/gut/script_parser.gd'): + get: return ScriptCollector.get_loaded() + set(val): pass +static var SignalWatcher = LazyLoader.new('res://addons/gut/signal_watcher.gd'): + get: return SignalWatcher.get_loaded() + set(val): pass +static var SingletonParser = LazyLoader.new('res://addons/gut/singleton_parser.gd'): + get: return SingletonParser.get_loaded() + set(val): pass +static var Spy = LazyLoader.new('res://addons/gut/spy.gd'): + get: return Spy.get_loaded() + set(val): pass +static var Strutils = LazyLoader.new('res://addons/gut/strutils.gd'): + get: return Strutils.get_loaded() + set(val): pass +static var Stubber = LazyLoader.new('res://addons/gut/stubber.gd'): + get: return Stubber.get_loaded() + set(val): pass +static var StubParams = LazyLoader.new('res://addons/gut/stub_params.gd'): + get: return StubParams.get_loaded() + set(val): pass +static var Stubs = LazyLoader.new('res://addons/gut/stubs.gd'): + get: return Stubs.get_loaded() + set(val): pass +static var Summary = LazyLoader.new('res://addons/gut/summary.gd'): + get: return Summary.get_loaded() + set(val): pass +static var Test = LazyLoader.new('res://addons/gut/test.gd'): + get: return Test.get_loaded() + set(val): pass +static var TestCollector = LazyLoader.new('res://addons/gut/test_collector.gd'): + get: return TestCollector.get_loaded() + set(val): pass +static var ThingCounter = LazyLoader.new('res://addons/gut/thing_counter.gd'): + get: return ThingCounter.get_loaded() + set(val): pass +static var UpdateDetector = LazyLoader.new('res://addons/gut/update_detector.gd'): + get: return UpdateDetector.get_loaded() + set(val): pass +# -------------------------------- + +static var gut_fonts = GutFonts.new() +static var avail_fonts = gut_fonts.get_font_names() +static var strutils = Strutils.new() + +static var version_numbers = VersionNumbers.new( + '9.7.1' # gut_versrion (source of truth) +) + + +static var warnings_at_start := { # WarningsManager dictionary + exclude_addons = true +} + +static var warnings_when_loading_test_scripts := { # WarningsManager dictionary + enable = false +} + + +# ------------------------------------------------------------------------------ +# Everything should get a logger through this. +# +# When running in test mode this will always return a new logger so that errors +# are not caused by getting bad warn/error/etc counts. +# ------------------------------------------------------------------------------ +static var _lgr = null +static func get_logger(): + if(_lgr == null): + _lgr = GutLogger.new() + return _lgr + +static var _error_tracker = null +static func get_error_tracker(): + if(_error_tracker == null): + _error_tracker = GutErrorTracker.new() + return _error_tracker + +static var inner_class_registry = InnerClassRegistry.new() + + +# ############################################################################## +# Methods +# ############################################################################## + + +# This must be static so that the scripts are counted. +static var _dyn_gdscript = DynamicGdScript.new() +# ############################################################################## +# Public Methods +# ############################################################################## +static func create_script_from_source(source, override_path=null): + var are_warnings_enabled = WarningsManager.are_warnings_enabled() + WarningsManager.enable_warnings(false) + + var DynamicScript = _dyn_gdscript.create_script_from_source(source, override_path) + if(typeof(DynamicScript) == TYPE_INT): + var l = get_logger() + l.error(str('Could not create script from source. Error: ', DynamicScript)) + l.info(str("Source Code:\n", add_line_numbers(source))) + + WarningsManager.enable_warnings(are_warnings_enabled) + + return DynamicScript + + +# Get the EditorInterface instance without having to make a direct reference to +# it. This allows for testing to be done on editor scripts that require it +# without having the parser error when you refer to it when not in the editor. +static func get_editor_interface(): + if(Engine.is_editor_hint()): + var inst = load("res://addons/gut/get_editor_interface.gd").new() + return inst.get_it() + else: + return null + + +static func godot_version_string(): + return version_numbers.make_godot_version_string() + + +static func is_godot_version(expected): + return VersionNumbers.VerNumTools.is_godot_version_eq(expected) + + +static func is_godot_version_gte(expected): + return VersionNumbers.VerNumTools.is_godot_version_gte(expected) + + +const INSTALL_OK_TEXT = 'Everything checks out' +static func make_install_check_text(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): + var text = INSTALL_OK_TEXT + if(!FileAccess.file_exists(template_paths.FUNCTION) or + !FileAccess.file_exists(template_paths.INIT) or + !FileAccess.file_exists(template_paths.SCRIPT)): + + text = '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.' + + return text + + + +# ------------------------------------------------------------------------------ +# Gets the value from an enum. +# - If passed an integer value as a string it will convert it to an int and +# processes the int value. +# - If the value is a float then it is converted to an int and then processes +# the int value +# - If the value is an int, or was converted to an int, then the enum is checked +# to see if it contains the value, if so then the value is returned. +# Otherwise the default is returned. +# - If the value is a string then it is uppercased and all spaces are replaced +# with underscores. It then checks to see if enum contains a key of that +# name. If so then the value for that key is returned, otherwise the default +# is returned. +# +# This description is longer than the code, you should have just read the code +# and the tests. +# ------------------------------------------------------------------------------ +static func get_enum_value(thing, e, default=null): + var to_return = default + + if(typeof(thing) == TYPE_STRING and str(thing.to_int()) == thing): + thing = thing.to_int() + elif(typeof(thing) == TYPE_FLOAT): + thing = int(thing) + + if(typeof(thing) == TYPE_STRING): + var converted = thing.to_upper().replace(' ', '_') + if(e.keys().has(converted)): + to_return = e[converted] + else: + if(e.values().has(thing)): + to_return = thing + + return to_return + + +# ------------------------------------------------------------------------------ +# return if_null if value is null otherwise return value +# ------------------------------------------------------------------------------ +static func nvl(value, if_null): + if(value == null): + return if_null + else: + return value + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +static func pretty_print(dict, indent = ' '): + print(JSON.stringify(dict, indent)) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +static func print_properties(props, thing, print_all_meta=false): + for i in range(props.size()): + var prop_name = props[i].name + var prop_value = thing.get(props[i].name) + var print_value = str(prop_value) + if(print_value.length() > 100): + print_value = print_value.substr(0, 97) + '...' + elif(print_value == ''): + print_value = 'EMPTY' + + print(prop_name, ' = ', print_value) + if(print_all_meta): + print(' ', props[i]) + + +static func print_method_list(thing): + for entry in thing.get_method_list(): + print("* ", entry.name) + + +# ------------------------------------------------------------------------------ +# Gets the value of the node_property 'script' from a PackedScene's root node. +# This does not assume the location of the root node in the PackedScene's node +# list. This also does not assume the index of the 'script' node property in +# a nodes's property list. +# ------------------------------------------------------------------------------ +static func get_scene_script_object(scene): + var state = scene.get_state() + var to_return = null + var root_node_path = NodePath(".") + var node_idx = 0 + + while(node_idx < state.get_node_count() and to_return == null): + if(state.get_node_path(node_idx) == root_node_path): + for i in range(state.get_node_property_count(node_idx)): + if(state.get_node_property_name(node_idx, i) == 'script'): + to_return = state.get_node_property_value(node_idx, i) + + node_idx += 1 + + return to_return + + +# ------------------------------------------------------------------------------ +# returns true if the object has been freed, false if not +# +# From what i've read, the weakref approach should work. It seems to work most +# of the time but sometimes it does not catch it. The str comparison seems to +# fill in the gaps. I've not seen any errors after adding that check. +# ------------------------------------------------------------------------------ +static func is_freed(obj): + var wr = weakref(obj) + return !(wr.get_ref() and str(obj) != '') + + +# ------------------------------------------------------------------------------ +# Pretty self explanitory. +# ------------------------------------------------------------------------------ +static func is_not_freed(obj): + return !is_freed(obj) + + +# ------------------------------------------------------------------------------ +# Checks if the passed in object is a GUT Double or Partial Double. +# ------------------------------------------------------------------------------ +static func is_double(obj): + var to_return = false + if(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)): + to_return = obj.has_method('__gutdbl_check_method__') + return to_return + + +# ------------------------------------------------------------------------------ +# Checks an object to see if it is a GDScriptNativeClass +# ------------------------------------------------------------------------------ +static func is_native_class(thing): + var it_is = false + if(typeof(thing) == TYPE_OBJECT): + it_is = str(thing).begins_with("= 0): + temp = decimal_value >> count + if(temp & 1): + binary_string = binary_string + "1" + else: + binary_string = binary_string + "0" + count -= 1 + + return binary_string + + +static func add_line_numbers(contents): + if(contents == null): + return '' + + var to_return = "" + var lines = contents.split("\n") + var line_num = 1 + for line in lines: + var line_str = str(line_num).lpad(6, ' ') + to_return += str(line_str, ' |', line, "\n") + line_num += 1 + return to_return + + +static func get_display_size(): + return Engine.get_main_loop().get_viewport().get_visible_rect() + + +static func find_method_meta(methods, method_name): + var meta = null + var idx = 0 + while (idx < methods.size() and meta == null): + var m = methods[idx] + if(m.name == method_name): + meta = m + idx += 1 + + return meta + + +static func get_method_meta(object, method_name): + if(object is GDScript): + return find_method_meta(object.get_script_method_list(), method_name) + elif(is_native_class(object)): + return find_method_meta(ClassDB.class_get_method_list(strutils.type2str(object)), method_name) + else: + return find_method_meta(object.get_method_list(), method_name) + + +static func is_singleton(thing): + if(typeof(thing) == TYPE_OBJECT): + return GodotSingletons.class_ref.has(thing) + elif(typeof(thing) == TYPE_STRING): + return GodotSingletons.names.has(thing) + else: + return false + + + + +static func is_singleton_double(thing): + return is_double(thing) and thing.__gutdbl_values.singleton_name != '' + + + +static func is_headless(): + return DisplayServer.get_name() == "headless" +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/utils.gd.uid b/addons/gut/utils.gd.uid new file mode 100644 index 0000000..c3d591d --- /dev/null +++ b/addons/gut/utils.gd.uid @@ -0,0 +1 @@ +uid://dbfbnvoq5osf2 diff --git a/addons/gut/version_conversion.gd b/addons/gut/version_conversion.gd new file mode 100644 index 0000000..114a946 --- /dev/null +++ b/addons/gut/version_conversion.gd @@ -0,0 +1,120 @@ +class GutConfigurationUpdater: + var EditorGlobals = load("res://addons/gut/gui/editor_globals.gd") + + func warn(message): + print('GUT Warning: ', message) + + + func info(message): + print("GUT Info: ", message) + + + func moved_file(from, to): + if(FileAccess.file_exists(from) and !FileAccess.file_exists(to)): + info(str('Copying [', from, '] to [', to, ']')) + var result = DirAccess.copy_absolute(from, to) + if(result != OK): + warn(str('Could not copy [', from, '] to [', to, ']')) + + if(FileAccess.file_exists(from) and FileAccess.file_exists(to)): + warn(str('File [', from, '] has been moved to [', to, "].\n You can delete ", from)) + + + func move_user_file(from, to): + if(from.begins_with('user://') and to.begins_with('user://')): + if(FileAccess.file_exists(from) and !FileAccess.file_exists(to)): + info(str('Moving [', from, '] to [', to, ']')) + var result = DirAccess.copy_absolute(from, to) + if(result == OK): + info(str(' ', 'Created ', to)) + result = DirAccess.remove_absolute(from) + if(result != OK): + warn(str(' ', 'Could not delete ', from)) + else: + info(str(' ', 'Deleted ', from)) + else: + warn(str(' ', 'Could not copy [', from, '] to [', to, ']')) + else: + warn(str('Attempt to move_user_file with files not in user:// ', from, '->', to)) + + + func remove_user_file(which): + if(which.begins_with('user://') and FileAccess.file_exists(which)): + info(str('Deleting obsolete file ', which)) + var result = DirAccess.remove_absolute(which) + if(result != OK): + warn(str(' ', 'Could not delete ', which)) + else: + info(str(' ', 'Deleted ', which)) + +class Gutv9_2_0: + extends GutConfigurationUpdater + + func validate(): + moved_file('res://.gut_editor_config.json', EditorGlobals.editor_run_gut_config_path) + moved_file('res://.gut_editor_shortcuts.cfg', EditorGlobals.editor_shortcuts_path) + remove_user_file('user://.gut_editor.bbcode') + remove_user_file('user://.gut_editor.json') + +# list=Array[Dictionary]([{ +# "base": &"RefCounted", +# "class": &"DynamicGutTest", +# "icon": "", +# "language": &"GDScript", +# "path": "res://test/resources/tools/dynamic_gut_test.gd" +# }, { +# "base": &"RefCounted", +# "class": &"GutDoubleTestInnerClasses", +# "icon": "", +# "language": &"GDScript", +# "path": "res://test/resources/doubler_test_objects/inner_classes.gd" +# }, ... ]) +static func get_missing_gut_class_names() -> Array: + var gut_class_names = [ + "GutErrorTracker", + "GutHookScript", + "GutInputFactory", + "GutInputSender", + "GutMain", + "GutStringUtils", + "GutTest", + "GutTrackedError", + "GutUtils", + ] + + var class_cach_path = 'res://.godot/global_script_class_cache.cfg' + var cfg = ConfigFile.new() + cfg.load(class_cach_path) + + var all_class_names = {} + var missing = [] + var class_cache_entries = cfg.get_value('', 'list', []) + + for entry in class_cache_entries: + if(entry.path.begins_with(&"res://addons/gut/")): + # print(entry["class"], ': ', entry["path"]) + all_class_names[entry["class"]] = entry + + for cn in gut_class_names: + if(!all_class_names.has(cn)): + missing.append(cn) + + return missing + + +static func error_if_not_all_classes_imported() -> bool: + var missing_class_names = get_missing_gut_class_names() + if(missing_class_names.size() > 0): + push_error(str("Some GUT class_names have not been imported. Please restart the Editor or run godot --headless --import\n", + "Missing class_names: ", + missing_class_names)) + return true + else: + return false + + + + +static func convert(): + var inst = Gutv9_2_0.new() + inst.validate() diff --git a/addons/gut/version_conversion.gd.uid b/addons/gut/version_conversion.gd.uid new file mode 100644 index 0000000..e69ecb7 --- /dev/null +++ b/addons/gut/version_conversion.gd.uid @@ -0,0 +1 @@ +uid://c8twdri50qrkb diff --git a/addons/gut/version_numbers.gd b/addons/gut/version_numbers.gd new file mode 100644 index 0000000..2476424 --- /dev/null +++ b/addons/gut/version_numbers.gd @@ -0,0 +1,127 @@ +# ############################################################################## +# +# ############################################################################## +class VerNumTools: + + static func _make_version_array_from_string(v): + var parts = Array(v.split('.')) + for i in range(parts.size()): + var int_val = parts[i].to_int() + if(str(int_val) == parts[i]): + parts[i] = parts[i].to_int() + return parts + + + static func make_version_array(v, min_spots=3): + var to_return = [] + if(typeof(v) == TYPE_STRING): + to_return = _make_version_array_from_string(v) + elif(typeof(v) == TYPE_DICTIONARY): + return [v.major, v.minor, v.patch] + elif(typeof(v) == TYPE_ARRAY): + to_return = v + + if(to_return.size() < min_spots): + for i in range(min_spots - to_return.size()): + to_return.append(0) + + return to_return + + + static func make_version_string(version_parts): + var to_return = 'x.x.x' + if(typeof(version_parts) == TYPE_ARRAY): + to_return = ".".join(version_parts) + elif(typeof(version_parts) == TYPE_DICTIONARY): + to_return = str(version_parts.major, '.', version_parts.minor, '.', version_parts.patch) + elif(typeof(version_parts) == TYPE_STRING): + to_return = version_parts + return to_return + + + static func is_version_gte(version, required): + var is_ok = null + var v = make_version_array(version) + var r = make_version_array(required) + + var idx = 0 + while(is_ok == null and idx < v.size() and idx < r.size()): + if(v[idx] > r[idx]): + is_ok = true + elif(v[idx] < r[idx]): + is_ok = false + + idx += 1 + + # still null means each index was the same. + return GutUtils.nvl(is_ok, true) + + static func is_version_lte(version, required): + var is_lt = null + var v = make_version_array(version) + var r = make_version_array(required) + + var idx = 0 + + while(is_lt == null and idx < v.size() and idx < r.size()): + if(v[idx] < r[idx]): + is_lt = true + elif(v[idx] > r[idx]): + is_lt = false + + idx += 1 + + # still null means each index was the same. + return GutUtils.nvl(is_lt, true) + + + static func is_version_eq(version, expected): + var version_array = make_version_array(version) + var expected_array = make_version_array(expected) + + if(expected_array.size() > version_array.size()): + return false + + var is_version = true + var i = 0 + while(i < expected_array.size() and i < version_array.size() and is_version): + if(expected_array[i] == version_array[i]): + i += 1 + else: + is_version = false + + return is_version + + + static func is_godot_version_eq(expected): + return VerNumTools.is_version_eq(Engine.get_version_info(), expected) + + + static func is_godot_version_gte(expected): + return VerNumTools.is_version_gte(Engine.get_version_info(), expected) + + + + +# ############################################################################## +# +# ############################################################################## +var gut_version = '0.0.0' + +func _init(gut_v = gut_version): + gut_version = gut_v + + + +# ------------------------------------------------------------------------------ +# Blurb of text with GUT and Godot versions. +# ------------------------------------------------------------------------------ +func get_version_text(): + var v_info = Engine.get_version_info() + var gut_version_info = str('GUT version: ', gut_version) + var godot_version_info = str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch) + return godot_version_info + "\n" + gut_version_info + + +func make_godot_version_string(): + return VerNumTools.make_version_string(Engine.get_version_info()) diff --git a/addons/gut/version_numbers.gd.uid b/addons/gut/version_numbers.gd.uid new file mode 100644 index 0000000..3a2dac3 --- /dev/null +++ b/addons/gut/version_numbers.gd.uid @@ -0,0 +1 @@ +uid://b4bb6lchs5uba diff --git a/addons/gut/versions.json b/addons/gut/versions.json new file mode 100644 index 0000000..4465533 --- /dev/null +++ b/addons/gut/versions.json @@ -0,0 +1,39 @@ +{ + "asset_library":"9.6.0", + "branches":{ + "main":{ + "godot_min":"4.6", + "godot_max":"4.6.999" + } + }, + "releases":{ + "9.7.1":{ + "godot_min":"4.7", + "godot_max":"4.7.999" + }, + "9.7.0":{ + "godot_min":"4.7", + "godot_max":"4.7.999" + }, + "9.6.1":{ + "godot_min":"4.6", + "godot_max":"4.6.999" + }, + "9.6.0":{ + "godot_min":"4.6", + "godot_max":"4.6.999" + }, + "9.5.0":{ + "godot_min":"4.5", + "godot_max":"4.5.999" + }, + "9.4.0":{ + "godot_min":"4.3", + "godot_max":"4.4.999" + }, + "9.3.0":{ + "godot_min":"4.2", + "godot_max":"4.2.999" + } + } +} diff --git a/addons/gut/warnings_manager.gd b/addons/gut/warnings_manager.gd new file mode 100644 index 0000000..e9aeee2 --- /dev/null +++ b/addons/gut/warnings_manager.gd @@ -0,0 +1,176 @@ +const IGNORE = 0 +const WARN = 1 +const ERROR = 2 + + +const WARNING_LOOKUP = { + IGNORE : 'IGNORE', + WARN : 'WARN', + ERROR : 'ERROR' +} + +const DIRECTORY_EXCLUDE = 0 +const DIRECTORY_INCLUDE = 1 + +const GDSCRIPT_WARNING = 'debug/gdscript/warnings/' +const DIRECTORY_RULES = 'debug/gdscript/warnings/directory_rules' + +const GUT_PATH = 'res://addons/gut' +const DYNAMIC_FILES_PATH = 'res://addons/gut/not_a_real_file' + + +# --------------------------------------- +# Static +# --------------------------------------- +static var _static_init_called = false +# This is static and set in _static_init so that we can get the current settings as +# soon as possible. +static var _project_warnings : Dictionary = {} + +static var _disabled = false +# should never be true, unless it is, but it shouldn't be. Whatever it is, it +# should stay the same for the entire run. Read only. +static var disabled = _disabled: + get: return _disabled + set(val):pass + +static var project_warnings := {} : + get: + # somehow this gets called before _project_warnings is initialized when + # loading a project in the editor. It causes an error stating that + # duplicate can't be called on nil. It seems there might be an + # implicit "get" call happening. Using push_error I saw a message + # in this method, but not one from _static_init upon loading the project + if(_static_init_called): + return _project_warnings.duplicate() + else: + return {} + set(val): pass + + +static func _static_init(): + _project_warnings = create_warnings_dictionary_from_project_settings() + _static_init_called = true + if(disabled): + print(""" + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! Warnings Manager has been disabled + !! + !! Do not push this up buddy + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + """.dedent()) + + +static func are_warnings_enabled(): + return ProjectSettings.get(str(GDSCRIPT_WARNING, 'enable')) + + +## Turn all warnings on/off. Use reset_warnings to restore the original value. +static func enable_warnings(should=true): + if(disabled): + return + ProjectSettings.set(str(GDSCRIPT_WARNING, 'enable'), should) + + +static func exclude_gut(should=true): + var val = DIRECTORY_INCLUDE + if(should): + val = DIRECTORY_EXCLUDE + var dir_rules = ProjectSettings.get(DIRECTORY_RULES) + dir_rules[GUT_PATH] = val + ProjectSettings.set(DIRECTORY_RULES, dir_rules) + +static func exclude_dynamic_files(): + var dir_rules = ProjectSettings.get(DIRECTORY_RULES) + dir_rules[DYNAMIC_FILES_PATH] = DIRECTORY_EXCLUDE + ProjectSettings.set(DIRECTORY_RULES, dir_rules) + + +## Resets warning settings to what they are set to in Project Settings +static func reset_warnings(): + apply_warnings_dictionary(_project_warnings) + + + +static func set_project_setting_warning(warning_name : String, value : Variant): + if(disabled): + return + + var property_name = str(GDSCRIPT_WARNING, warning_name) + # This check will generate a warning if the setting does not exist + if(property_name in ProjectSettings): + ProjectSettings.set(property_name, value) + + +static func apply_warnings_dictionary(warning_values : Dictionary): + if(disabled): + return + + for key in warning_values: + set_project_setting_warning(key, warning_values[key]) + + +static func create_ignore_all_dictionary(): + return replace_warnings_values(project_warnings, -1, IGNORE) + + +static func create_warn_all_warnings_dictionary(): + return replace_warnings_values(project_warnings, -1, WARN) + + +static func replace_warnings_with_ignore(dict): + return replace_warnings_values(dict, WARN, IGNORE) + + +static func replace_errors_with_warnings(dict): + return replace_warnings_values(dict, ERROR, WARN) + + +static func replace_warnings_values(dict, replace_this, with_this): + var to_return = dict.duplicate() + for key in to_return: + if(typeof(to_return[key]) == TYPE_INT and (replace_this == -1 or to_return[key] == replace_this)): + to_return[key] = with_this + return to_return + + +static func create_warnings_dictionary_from_project_settings() -> Dictionary : + var props = ProjectSettings.get_property_list() + var to_return = {} + for i in props.size(): + if(props[i].name.begins_with(GDSCRIPT_WARNING)): + var prop_name = props[i].name.replace(GDSCRIPT_WARNING, '') + to_return[prop_name] = ProjectSettings.get(props[i].name) + return to_return + + +static func print_warnings_dictionary(which : Dictionary): + var is_valid = true + for key in which: + var value_str = str(which[key]) + if(_project_warnings.has(key)): + if(typeof(which[key]) == TYPE_INT): + if(WARNING_LOOKUP.has(which[key])): + value_str = WARNING_LOOKUP[which[key]] + else: + push_warning(str(which[key], ' is not a valid value for ', key)) + is_valid = false + else: + push_warning(str(key, ' is not a valid warning setting')) + is_valid = false + var s = str(key, ' = ', value_str) + print(s) + return is_valid + + +static func load_script_ignoring_all_warnings(path : String) -> Variant: + return load_script_using_custom_warnings(path, create_ignore_all_dictionary()) + + +static func load_script_using_custom_warnings(path : String, warnings_dictionary : Dictionary) -> Variant: + var current_warns = create_warnings_dictionary_from_project_settings() + apply_warnings_dictionary(warnings_dictionary) + var s = load(path) + apply_warnings_dictionary(current_warns) + + return s diff --git a/addons/gut/warnings_manager.gd.uid b/addons/gut/warnings_manager.gd.uid new file mode 100644 index 0000000..bb86e6f --- /dev/null +++ b/addons/gut/warnings_manager.gd.uid @@ -0,0 +1 @@ +uid://blo71surxlb13 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c29fe33 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,103 @@ +# Architecture + +## The shape + +``` + ┌──────────────────────────────────────────┐ + │ src/sim/ -- plain RefCounted objects │ + authoritative │ SimWorld, SimPlayer, SimEnemy, SimBoss, │ replica + (server) │ BulletPool, emitters │ (client) + │ no nodes · no physics · no rendering │ + └───────────────┬──────────────────────────┘ + │ + ┌────────────────────────┼────────────────────────┐ + │ │ │ +┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ +│ src/instances/ │ │ src/net/ │ │ src/view/ ui/ │ +│ lobby, dungeon │ │ codec, server & │ │ read-only draw │ +│ progression │ │ client runtimes │ │ │ +└────────────────┘ └────────┬────────┘ └─────────────────┘ + │ + ┌────────▼────────┐ + │ src/autoload/ │ + │ net.gd (RPC) │ + └─────────────────┘ +``` + +Dependencies point inwards only. `src/sim/` imports nothing from `net`, `view`, +`ui` or `instances`, and never touches the `Net` autoload. That is what makes +the simulation runnable in a unit test, in a headless server and inside a client +without a single conditional. + +## Why the simulation is not made of nodes + +Godot's instinct is `CharacterBody2D` + `Area2D` + `MultiplayerSynchronizer`. +This project deliberately does not, for four reasons: + +1. **Bullet count.** Hundreds of live bullets. A node and an `Area2D` each would + cost more than the entire simulation does. `BulletPool` is parallel packed + arrays; a tick over ~350 bullets costs 0.24 ms including AI and hit checks. +2. **Headless cost.** The dedicated server allocates no nodes and touches no + physics server. +3. **Testability.** A `SimWorld` is `SimWorld.new()`. The suite drives thousands + of ticks in ~2 s with no SceneTree, no awaits and no frame timing. +4. **Replication control.** `MultiplayerSynchronizer` is excellent for + prototyping and wrong for this shape: it would replicate per-bullet state and + saturate the link. See [NETCODE.md](NETCODE.md). + +The trade is that there is no physics engine — no arbitrary collision shapes, no +tilemap collision. The arena is a rectangle and every hit is circle-vs-circle. +For a bullet hell that is not a limitation; hitboxes in the genre are circles by +convention anyway. A dungeon with real geometry would need a static collision +representation added to `SimWorld`, not a switch back to nodes. + +## Tick + +Everything runs on Godot's physics tick, pinned to 60 Hz in `src/main.gd` +(asserted in code, because `project.godot` drops settings equal to the engine +default). `SimConfig` holds every constant both sides must agree on; per-machine +options live in `GameOpts` and must never affect the simulation. + +## Content pipeline + +``` +BulletEmitter (Resource, abstract) +├── RingEmitter count, arc, spin per shot -> rings and spirals +├── AimedSpreadEmitter fan at nearest player -> punishes standing still +├── WallGapEmitter curtain with a sliding gap -> forces a committed dodge +└── ArcSweepEmitter rotating arms -> forces tracking + +EnemyDef = stats + movement enum + emitters +BossPhase = hp threshold + telegraph + looping timeline of emitters +BossDef = stats + ordered phases +``` + +`SimWorld._run_emitters()` is shared by enemies and bosses, so any pattern can +be dropped on either. A boss is four phases layering one idea at a time; a new +boss is a new function in `src/content/content.gd` and zero simulation changes. + +## Entry point + +One executable, one `src/main.tscn`. `GameOpts.parse()` reads the command line +after `--`: + +- `--server` → `ServerRuntime`, no view, `max_fps` pinned to the tick rate. +- `--join` / `--bot` → connect immediately, skipping the menu. +- otherwise → the connect menu, which can also start a listen server. + +## File map + +| File | Role | +| --- | --- | +| `src/core/sim_config.gd` | Every constant server and client must agree on. | +| `src/core/movement.gd` | Pure movement + overlap helpers. Shared by prediction. | +| `src/core/game_log.gd`, `game_opts.gd` | Static; usable from tools and tests. | +| `src/sim/sim_world.gd` | The simulation. Authority flag decides what runs. | +| `src/sim/bullet_pool.gd` | Struct-of-arrays bullet storage and integration. | +| `src/sim/input_frame.gd` | The only thing a client may assert about itself. | +| `src/net/net_codec.gd` | Snapshot / event / input binary codecs. | +| `src/net/server_runtime.gd` | Instances, ticking, transfers, broadcast. | +| `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. | +| `src/instances/instance.gd` | Lobby hub and dungeon progression. | +| `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. | +| `src/view/bullet_renderer.gd` | The whole bullet field in one MultiMesh draw call. | diff --git a/docs/NETCODE.md b/docs/NETCODE.md new file mode 100644 index 0000000..62cb218 --- /dev/null +++ b/docs/NETCODE.md @@ -0,0 +1,156 @@ +# Netcode + +## Model + +Authoritative dedicated server, clients send input only, with client-side +prediction and reconciliation for the local player. + +``` +client server (60 Hz) + | | + |-- InputFrame (60 Hz, x3) ---->| queue_input(): validate, drop, sort + | move, aim, 3 button bits | SimWorld.step(): move, AI, emitters, + | | integrate bullets, resolve hits + |<-- events (reliable, ch 3) ---| bullet spawns/despawns, hits, deaths + |<-- snapshot (unrel., ch 2) ---| players, enemies, boss @ 20 Hz + | | + predict locally, reconcile authoritative for everything +``` + +The client has no message that expresses a position, a hit, damage taken, or a +completed escape. This is deliberate and stronger than validating such messages +after the fact: there is no code path to exploit, only intent to interpret. + +## Why bullets are not replicated as state + +A bullet hell has hundreds of live bullets — this project peaks at ~350 with one +boss and four players. Sending a position per bullet per snapshot would be +roughly 350 × 8 bytes × 20 Hz ≈ 56 KB/s per client and would grow with the +pattern density, which is the one thing you want freedom to increase. + +Instead the server sends **one spawn event per bullet, once**: uid, position, +velocity, radius, lifetime, kind, team, acceleration, turn rate (~36 bytes). +Both sides then run the identical integration in `BulletPool.step()`. A bullet +that expires or leaves the arena dies on both sides with no traffic at all. + +The only bullets that need a despawn message are the ones killed early by a hit, +because that is the one outcome a client cannot derive. + +Cost at the same peak: a few KB/s, dominated by whatever the patterns are +actually firing rather than by how much is in the air. + +`tests/integration/test_replica_parity.gd` is the test that keeps this honest. +It runs the server and a replica side by side for 900 ticks of a boss fight — +once in-process, once with every event encoded and decoded through the real wire +format — and asserts that every live bullet matches within 0.01 px (1 px through +float32 on the wire). If that test fails, the bullets on screen are no longer +the bullets that can kill you. + +Note what this does **not** require: lockstep determinism. Clients simulate +bullets for display only. The server never reads a client's bullet state, so +float drift between machines is a cosmetic concern, not a correctness one. + +## Hit validation + +All in `SimWorld._resolve_bullet_hits()` and `_resolve_contact_damage()`, server +only, circle-vs-circle: + +- **Enemy bullet → player:** skipped entirely while the player has i-frames, so + a dense pattern costs one hit, not thirty. The bullet is consumed and a + `BULLET_DESPAWN` goes out. +- **Player bullet → boss, then enemies:** first overlap consumes the bullet. + Boss damage is scaled by the current phase's `damage_taken_mult`. +- **Enemy body → player:** contact damage, same i-frame gate. + +There is no lag compensation and no rewind. For a bullet hell that is the right +call: what the player is dodging is their own position against a bullet field, +and rewinding the world to a shooter's view would mean a player who dodged on +their screen still gets hit. The cost is that a high-ping player's shots at a +moving enemy land where the enemy *was* on the server. Enemies are large and +slow relative to bullet speed, so this reads as latency rather than unfairness. +If it becomes a complaint, the fix is to lag-compensate **player bullets against +enemies only**, never enemy bullets against players. + +## Input validation + +`SimWorld.queue_input()` is the single audit point. It drops: + +| Case | Guard | +| --- | --- | +| Replay of a consumed tick | `f.tick <= p.last_input_tick` | +| Stale input | `f.tick < tick - INPUT_MAX_AGE` | +| Input claiming the future | `f.tick > tick + INPUT_MAX_LEAD` | +| Flood | queue capped at `INPUT_MAX_AGE`, oldest dropped | + +Beyond that, the wire format itself constrains the cheat surface: the move +vector is two signed bytes at 1/100 precision, and `Movement.sanitize_move()` +clamps to the unit disc, so no packet can express extra speed. Fire rate is a +server-side cooldown; holding the button every tick fires at exactly the same +rate as tapping it correctly. + +A client that stops sending coasts on its last input for `INPUT_MAX_AGE` ticks +and then stops, so a dropped connection does not leave a player sliding. + +## Prediction and reconciliation + +`ClientRuntime` keeps every unacknowledged `InputFrame`. Each snapshot echoes +`last_input_tick` per player; on receipt the client discards acknowledged +frames, takes the server's authoritative position, and replays the rest through +the same `Movement.step_player()` the server used. Divergence over 24 px snaps; +smaller errors lerp at 0.3 so ordinary jitter does not read as rubber-banding. + +Remote players, enemies and the boss are interpolated between the last two +snapshots. Bullets are not interpolated — they are simulated, and a spawn event +carries the server tick it was generated at so the client fast-forwards the +bullet by the packet's flight time (`BulletPool.advance_slot()`) instead of +popping it in at the muzzle a round-trip late. + +## Channels + +ENet channels keep a burst of reliable bullet events from head-of-line blocking +the snapshot stream: + +| Channel | Traffic | Mode | +| --- | --- | --- | +| 1 | handshake, instance transitions | reliable | +| 2 | snapshots | unreliable, newest wins | +| 3 | bullet spawns/despawns, hits | reliable ordered | +| 4 | client input | unreliable ordered | + +`Protocol.VERSION` is checked at handshake and mismatched clients are rejected +with a reason, rather than desyncing later. + +## Instances + +One server process hosts the lobby hub plus every concurrent dungeon. An +`Instance` is a `SimWorld` and a peer list; messages are addressed to the peers +of one instance, so a player never receives traffic for a world they are not in. +At ~0.24 ms/tick per instance, the limit is bandwidth and player count long +before it is CPU. + +Transfers (portal in, escape out, dungeon cleared) are driven by server-only +events that `NetCodec` refuses to serialise, so the decision cannot leak to a +client as something it might try to send back. + +## The listen server + +"Host and play" runs a `ServerRuntime` and a `ClientRuntime` in one process. The +client is peer 1 and goes through the identical code path — it still sends input +and still learns outcomes from snapshots. Only the transport is short-circuited, +in `Net.send_*` / `Net.send_input`, which check for the local peer and call the +handler directly instead of going through ENet. No simulation code knows the +difference, so a bug cannot hide in a "host-only" branch. + +## Known gaps + +- No lag compensation (see above — deliberate, with a stated escape hatch). +- No snapshot delta compression. Full state at 20 Hz is fine at this actor + count; delta-encoding against the last acknowledged snapshot is the next step + if enemy counts grow. +- No encryption or authentication. `ENetMultiplayerPeer` supports DTLS; wire it + up before exposing a server to the internet. +- Bullet spawn events are per-bullet. Sending *pattern* events instead (emitter + id + tick + seed, clients regenerate) would cut bullet traffic by roughly the + count of each emitter's shot. Worth doing only if bandwidth becomes the + binding constraint — it couples the client to emitter behaviour, which the + current design deliberately avoids. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..13317f8 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,56 @@ +# Roadmap + +## Built + +| MVP requirement | State | +| --- | --- | +| Top-down 2D twin-stick bullet hell | Movement, aim, fire, i-frames, death and respawn; MultiMesh bullet rendering. | +| Multiplayer with server-side hit validation | Authoritative dedicated server; clients send input only; all hits resolved server-side for both players and enemies. No PvP. | +| Simple, predictable enemies | Drifter, Turret, Stalker, plus a lobby dummy. Five movement behaviours, all closed-form. | +| Stationary boss, adaptable format | The Warden of the Fold: four phases, data-defined. A new boss is one function and zero simulation changes. | +| Lobby hub with dungeon entry | Persistent lobby instance, portal, party forming window, dungeons opened on demand. | +| Emergency escape | Three-second channel, cancelled by damage or release, server-owned. | + +78 tests plus an end-to-end smoke test over a real socket. + +## Next, in rough order of value + +**1. Make the dungeon a place.** Right now it is one arena and three stages. +Rooms, doors, and a `SimWorld` with static geometry — which means adding a +collision representation for walls (segment-vs-circle for players, segment +crossing for bullets) since there is no physics engine to lean on. + +**2. Art and feel.** Everything is drawn with `draw_circle` and a generated dot +texture. Sprites, hit flashes, screen shake, muzzle flashes, death effects, and +sound. None of it touches the simulation — this is entirely `src/view/`. + +**3. A second boss.** The format claims to be reusable; the way to find out is +to use it. A mobile boss will exercise `BossDef.stationary`, which the runtime +already reads but no content uses yet. + +**4. Progression.** Loot, character stats, persistence. Needs a decision on +storage: for a dedicated server, player state belongs server-side in a database, +not in a client save file. + +**5. Netcode hardening.** In the order they will actually matter: + - DTLS on `ENetMultiplayerPeer` before any public server. + - Authentication; `peer_names` is currently client-supplied and trusted for + display only, which is fine now and will not be once there is progression. + - Snapshot delta compression, once enemy counts grow. + - Interest management inside an instance, once arenas are larger than a + screen. + +**6. Scale.** The simulation costs ~1.4% of a core per instance, so the ceiling +is bandwidth and process supervision, not CPU. A shard manager that runs several +server processes behind a lobby-of-lobbies is the shape, but it is premature +until there is a game to fill it. + +## Deliberately not done + +- **Lag compensation.** Discussed in [NETCODE.md](NETCODE.md): rewinding for a + shooter's view would mean a player who dodged still gets hit, which is the + wrong trade for this genre. +- **Pattern-level bullet replication.** A large bandwidth win that couples the + client to emitter behaviour. Not worth it until bandwidth binds. +- **`MultiplayerSynchronizer` / `MultiplayerSpawner`.** Right tools, wrong shape + for a bullet hell. See [ARCHITECTURE.md](ARCHITECTURE.md). diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md new file mode 100644 index 0000000..b81b529 --- /dev/null +++ b/docs/WORKFLOW.md @@ -0,0 +1,177 @@ +# Agent-assisted Godot development + +Research notes and the resulting setup for this repo. The short version: Godot +is an editor-centric engine and a coding agent is a text-centric tool, so the +whole workflow is built around making the game fully runnable, inspectable and +verifiable from the command line — with the editor as an optional viewer rather +than a required step. + +## The core problem + +An agent editing a Godot project cannot see the editor. It cannot click play, +cannot look at the scene dock, and cannot tell whether a `.tscn` it wrote is +valid. Left alone it will produce code that looks right and does not run. + +Everything below exists to close that gap. Three properties matter: + +1. **A fast, honest failure signal.** Parse errors must surface in seconds, from + the CLI, with the engine's own words. +2. **A way to actually play the game without a human.** Not "the tests pass" — + the real binary, the real socket, the real loop. +3. **Source of truth in text.** Binary scenes and inspector-tuned resources are + invisible to review and produce meaningless diffs. + +## Godot's CLI, as an agent uses it + +| Command | Use | +| --- | --- | +| `godot --headless --path . --import` | Rebuild the import + **global class cache**. Required after adding any `class_name`. ~2s. | +| `godot --headless --path . res://scene.tscn` | Run a scene headless with autoloads alive. | +| `godot --headless --path . --script tools/x.gd` | Run a `SceneTree` script. **No autoloads.** | +| `godot --headless --check-only -s file.gd` | Parse one file. | +| `godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://tests -gexit` | Run the test suite. | +| `godot --headless --path . --export-release ` | Build. | + +Four things cost real time to discover, so they are worth stating plainly: + +- **The global class cache is not built by running the game.** It lives in + `.godot/global_script_class_cache.cfg` and is written by the editor or by + `--import`. Add a `class_name`, run your code, and every reference to it fails + with `Identifier "Foo" not declared in the current scope` — a message that + looks exactly like a typo and is not one. `tools/check.sh` runs `--import` + first for this reason. +- **Autoload singletons do not exist under `--script`.** `--script` replaces the + main loop, so autoload names are unresolved identifiers at compile time. Any + tool that needs an autoload has to run as a scene instead. The practical + consequence for design: keep autoloads to the minimum that genuinely needs to + be a node. This repo has exactly one (`Net`, because RPC routing is by node + path); logging and CLI options are static classes, which makes them usable + from tests and tools alike. +- **`ResourceLoader.load()` on a script with a parse error can return a non-null + object.** Detecting failures by null-checking the return value silently passes. + Read the engine's stderr instead — that is what `tools/check.sh` does. +- **Never call `Script.reload()` on the script currently executing.** It hangs + the engine with no output. + +## What this repo does about it + +### `tools/check.sh` — the inner loop + +Refreshes the class cache, loads every `.gd` in the project, greps the engine's +stderr for `Parse Error` / `SCRIPT ERROR` / `Failed to load script`, and exits +non-zero on any hit. About five seconds. This is the command to run after every +edit; it catches the entire class of "looks right, does not compile" mistakes +that an agent produces most often. + +### `tools/test.sh` — GUT, headless + +[GUT 9.7.1](https://github.com/bitwes/Gut) is vendored into `addons/gut/`. +Chosen over [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) because this +project is GDScript-only, where GUT is the lighter and simpler of the two; +gdUnit4 is the better pick when C# is in play or when you want its scene-runner +utilities. Either runs headless in CI. + +The suite runs in about two seconds because of an architectural choice, not a +testing trick: **the entire simulation is plain `RefCounted` objects** — no +nodes, no physics server, no rendering. A test constructs a `SimWorld`, drives +a thousand ticks, and asserts. Nothing to await, nothing to instantiate, no +frame timing. If the simulation had been built out of `CharacterBody2D` and +`Area2D`, every one of these tests would need a live SceneTree and would be +slower and flakier by an order of magnitude. + +That is the single highest-leverage decision in this repo for agent-assisted +work, and it is worth stating as a general rule: **keep game logic out of nodes.** +Nodes are for presentation and input. Logic in plain objects is testable, +diffable, reviewable, and runs on a headless server for free. + +### `tools/smoke.sh` — the thing tests cannot do + +Boots the real dedicated server, connects two scripted bot clients over a real +ENet socket, and asserts on the server and client logs that the whole loop +happened: handshake, lobby, portal into a dungeon, emergency escape back out. + +Unit tests structurally cannot cover RPC wiring, codec round-trips over the +wire, instance transfers or client reconciliation. This does, in ~35 seconds, +with no display. The bot input lives in `ClientRuntime._bot_input()` behind +`--bot`, so the "player" driving it is the same code path a human uses. + +This is the pattern to reach for whenever an agent needs to verify something +interactive: give the program a scripted-input mode and a `--autoquit`, run it +headless, and assert on structured log lines. Log markers like `SERVER_READY` +exist specifically to be grepped. + +### `tools/bench.gd` — performance as a number + +Runs the simulation with no transport and no rendering and prints milliseconds +per tick. Current numbers, 4 players, 60s of play: + +``` +boss fight 0.237 ms/tick peak 352 bullets headroom x70 +trash wave 0.179 ms/tick peak 145 bullets headroom x93 +``` + +A 60 Hz tick has a 16.6 ms budget, so one instance uses ~1.4% of one core. That +is the measurement that says a single server process can host dozens of +concurrent dungeons, and it took ten seconds to get because the simulation has +no engine dependencies. + +## Content as code, not as `.tres` + +Enemies and bosses are built by GDScript functions in `src/content/content.gd`, +not authored as `.tres` files. For agent-assisted work this is the right default: + +- A boss is a readable diff. A `.tres` full of `SubResource` ids is not. +- No resource UID churn in version control. +- A test can build content inline without touching the filesystem. +- The agent can write a boss without an editor. + +`tools/export_content.gd` writes `.tres` copies into `resources/` for anyone who +wants to tune numbers in the inspector, with the direction of truth documented: +code wins, port inspector changes back. + +The same reasoning applies to scenes. This project has three `.tscn` files, each +a handful of nodes. Anything dynamic — the HUD, the menu — is built in code, +because a scene file for it would only be one more thing to keep in sync. + +## Godot MCP servers + +An MCP server lets an agent talk to a live Godot editor: inspect the scene tree, +read node properties, create nodes, run the project and read its output. The +most established option is +[Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp) (~5.5k stars, +Node ≥18): + +```bash +claude mcp add godot -- npx @coding-solo/godot-mcp +``` + +It exposes `launch_editor`, `run_project`, `stop_project`, `get_debug_output`, +`get_godot_version`, `get_project_info`, `create_scene`, `add_node`, +`load_sprite`, `save_scene`, `export_mesh_library`, and UID helpers for 4.4+. +Set `GODOT_PATH` if `godot` is not on `PATH`. + +**Where it earns its place:** projects with large hand-authored scene trees, +where the agent otherwise has to guess node names and paths. Being able to read +the real tree instead of inferring it is the biggest single quality jump in that +situation. + +**Where it does not:** this repo. `run_project` and `get_debug_output` are what +`tools/server.sh` and `tools/smoke.sh` already do, more precisely and with +assertions attached; and the scene-authoring tools matter little when there are +three small scenes. It also adds a live-editor dependency, and editor +auto-reload racing against agent file writes is a known source of stale state. + +The honest recommendation: **start with the headless CLI loop, add the MCP when +scene inspection becomes the bottleneck.** It is one command to add later. + +## CI + +`.github/workflows/ci.yml` runs check → test → smoke on a container image with +Godot preinstalled. All three are headless, so nothing extra is needed. + +## Sources + +- [Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp) +- [GUT](https://github.com/bitwes/Gut) · [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) +- [Godot: high-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) +- [Godot: command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html) diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9026ce9 --- /dev/null +++ b/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..0b2f3fd --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cf2exh1nt1d6c" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..1ea3541 --- /dev/null +++ b/project.godot @@ -0,0 +1,88 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Transcience" +config/description="Top-down twin-stick bullet-hell with a server-authoritative dedicated server." +run/main_scene="res://src/main.tscn" +config/features=PackedStringArray("4.7", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +Net="*res://src/autoload/net.gd" + +[debug] + +gdscript/warnings/untyped_declaration=1 +gdscript/warnings/unsafe_property_access=1 +gdscript/warnings/unsafe_method_access=1 + +[display] + +window/size/viewport_width=1280 +window/size/viewport_height=720 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gut/plugin.cfg") + +[input] + +move_up={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_down={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +fire={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +emergency_escape={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +interact={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + +[rendering] + +textures/canvas_textures/default_texture_filter=0 +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +environment/defaults/default_clear_color=Color(0.05, 0.05, 0.08, 1) diff --git a/resources/bosses/warden.tres b/resources/bosses/warden.tres new file mode 100644 index 0000000..7a15672 --- /dev/null +++ b/resources/bosses/warden.tres @@ -0,0 +1,157 @@ +[gd_resource type="Resource" script_class="BossDef" format=3] + +[ext_resource type="Script" path="res://src/actors/boss/boss_phase.gd" id="1_7m6h6"] +[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="2_b0fvw"] +[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="3_f5abt"] +[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="4_tmo3e"] +[ext_resource type="Script" path="res://src/sim/patterns/wall_gap_emitter.gd" id="5_rswgd"] +[ext_resource type="Script" path="res://src/sim/patterns/arc_sweep_emitter.gd" id="6_v02no"] +[ext_resource type="Script" path="res://src/actors/boss/boss_def.gd" id="7_x6ufy"] + +[sub_resource type="Resource" id="Resource_bbcbd"] +script = ExtResource("3_f5abt") +count = 14 +spin_per_shot_deg = 9.0 +muzzle_offset = 46.0 +interval = 45 +speed = 135.0 +radius = 8.0 +lifetime = 420 +damage = 14 + +[sub_resource type="Resource" id="Resource_axg03"] +script = ExtResource("4_tmo3e") +spread_deg = 26.0 +muzzle_offset = 46.0 +start_tick = 120 +interval = 150 +speed = 210.0 +radius = 6.0 +kind = 2 + +[sub_resource type="Resource" id="Resource_s17mk"] +script = ExtResource("1_7m6h6") +name = "Opening Fold" +loop_ticks = 480 +telegraph_ticks = 60 +emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_bbcbd"), SubResource("Resource_axg03")]) + +[sub_resource type="Resource" id="Resource_0452f"] +script = ExtResource("3_f5abt") +count = 4 +spin_per_shot_deg = 23.0 +muzzle_offset = 46.0 +interval = 9 +speed = 120.0 +lifetime = 480 + +[sub_resource type="Resource" id="Resource_1p7x0"] +script = ExtResource("5_rswgd") +count = 20 +gap_step = 7 +start_tick = 90 +interval = 170 +speed = 175.0 +radius = 8.0 +damage = 16 +kind = 3 + +[sub_resource type="Resource" id="Resource_a0mrn"] +script = ExtResource("1_7m6h6") +name = "Sliding Lattice" +enter_at_hp_fraction = 0.72 +telegraph_ticks = 50 +emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_0452f"), SubResource("Resource_1p7x0")]) + +[sub_resource type="Resource" id="Resource_pfjxl"] +script = ExtResource("6_v02no") +arms = 3 +bullets_per_arm = 2 +arm_spacing = 26.0 +sweep_deg = 95.0 +sweep_period = 5.0 +interval = 5 +speed = 165.0 +lifetime = 400 +damage = 13 + +[sub_resource type="Resource" id="Resource_txml3"] +script = ExtResource("4_tmo3e") +count = 7 +spread_deg = 40.0 +jitter_deg = 3.0 +edge_speed_bonus = 0.25 +start_tick = 60 +interval = 110 +speed = 195.0 +radius = 6.0 +kind = 2 + +[sub_resource type="Resource" id="Resource_k1e8f"] +script = ExtResource("3_f5abt") +count = 18 +spin_per_shot_deg = -14.0 +start_tick = 240 +end_tick = 600 +interval = 60 +speed = 105.0 +lifetime = 420 + +[sub_resource type="Resource" id="Resource_4sp1g"] +script = ExtResource("1_7m6h6") +name = "Reaping Arms" +enter_at_hp_fraction = 0.42 +loop_ticks = 720 +damage_taken_mult = 1.15 +emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_pfjxl"), SubResource("Resource_txml3"), SubResource("Resource_k1e8f")]) + +[sub_resource type="Resource" id="Resource_aldxl"] +script = ExtResource("3_f5abt") +count = 5 +spin_per_shot_deg = 31.0 +muzzle_offset = 46.0 +interval = 11 +speed = 130.0 +turn_deg = 0.55 +lifetime = 400 +damage = 15 + +[sub_resource type="Resource" id="Resource_3fm5w"] +script = ExtResource("5_rswgd") +direction = 2 +count = 16 +start_tick = 40 +interval = 130 +speed = 200.0 +radius = 8.0 +damage = 18 +kind = 3 + +[sub_resource type="Resource" id="Resource_20c8f"] +script = ExtResource("4_tmo3e") +count = 3 +spread_deg = 10.0 +start_tick = 100 +interval = 80 +speed = 300.0 +radius = 5.0 +damage = 16 +kind = 2 + +[sub_resource type="Resource" id="Resource_ymr56"] +script = ExtResource("1_7m6h6") +name = "Collapse" +enter_at_hp_fraction = 0.16 +loop_ticks = 420 +telegraph_ticks = 60 +damage_taken_mult = 1.3 +emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_aldxl"), SubResource("Resource_3fm5w"), SubResource("Resource_20c8f")]) + +[resource] +script = ExtResource("7_x6ufy") +id = &"warden" +display_name = "Warden of the Fold" +max_hp = 3600 +radius = 42.0 +spawn_pos = Vector2(0, -150) +phases = Array[ExtResource("1_7m6h6")]([SubResource("Resource_s17mk"), SubResource("Resource_a0mrn"), SubResource("Resource_4sp1g"), SubResource("Resource_ymr56")]) diff --git a/resources/enemies/drifter.tres b/resources/enemies/drifter.tres new file mode 100644 index 0000000..ed4a123 --- /dev/null +++ b/resources/enemies/drifter.tres @@ -0,0 +1,23 @@ +[gd_resource type="Resource" script_class="EnemyDef" format=3] + +[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_801wv"] +[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="2_bjhtk"] +[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_e6oj1"] + +[sub_resource type="Resource" id="Resource_vpd04"] +script = ExtResource("2_bjhtk") +count = 3 +spread_deg = 20.0 +interval = 120 +radius = 6.0 +lifetime = 240 +damage = 10 + +[resource] +script = ExtResource("3_e6oj1") +id = &"drifter" +display_name = "Drifter" +contact_damage = 8 +speed = 55.0 +emitters = Array[ExtResource("1_801wv")]([SubResource("Resource_vpd04")]) +pattern_loop_ticks = 120 diff --git a/resources/enemies/dummy.tres b/resources/enemies/dummy.tres new file mode 100644 index 0000000..08779bf --- /dev/null +++ b/resources/enemies/dummy.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="EnemyDef" format=3] + +[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_ajtny"] +[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="2_i7w5w"] + +[resource] +script = ExtResource("2_i7w5w") +id = &"dummy" +display_name = "Target Dummy" +max_hp = 100000 +radius = 20.0 +contact_damage = 0 +move = 0 +visual = 3 diff --git a/resources/enemies/stalker.tres b/resources/enemies/stalker.tres new file mode 100644 index 0000000..e58e0a1 --- /dev/null +++ b/resources/enemies/stalker.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="EnemyDef" format=3] + +[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_dkoo8"] +[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="2_t1lyl"] + +[resource] +script = ExtResource("2_t1lyl") +id = &"stalker" +display_name = "Stalker" +max_hp = 30 +radius = 12.0 +contact_damage = 16 +move = 3 +speed = 95.0 +retarget_interval = 20 +visual = 2 diff --git a/resources/enemies/turret.tres b/resources/enemies/turret.tres new file mode 100644 index 0000000..2d6c5bd --- /dev/null +++ b/resources/enemies/turret.tres @@ -0,0 +1,25 @@ +[gd_resource type="Resource" script_class="EnemyDef" format=3] + +[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_asksf"] +[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="2_7m5d5"] +[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_5udip"] + +[sub_resource type="Resource" id="Resource_hc01i"] +script = ExtResource("2_7m5d5") +count = 10 +spin_per_shot_deg = 18.0 +interval = 150 +speed = 130.0 + +[resource] +script = ExtResource("3_5udip") +id = &"turret" +display_name = "Turret" +max_hp = 70 +radius = 16.0 +contact_damage = 0 +move = 0 +speed = 0.0 +visual = 1 +emitters = Array[ExtResource("1_asksf")]([SubResource("Resource_hc01i")]) +pattern_loop_ticks = 150 diff --git a/src/actors/boss/boss_def.gd b/src/actors/boss/boss_def.gd new file mode 100644 index 0000000..96bc0e4 --- /dev/null +++ b/src/actors/boss/boss_def.gd @@ -0,0 +1,24 @@ +class_name BossDef +extends Resource +## A whole boss. Adding a boss to the game is writing one of these plus its +## emitter list -- there is no per-boss code path anywhere in the simulation. + +@export var id: StringName = &"boss" +@export var display_name: String = "Boss" +@export var max_hp: int = 4000 +@export var radius: float = 44.0 +## Stationary bosses are the MVP shape; the field exists so a later boss can +## move without changing the runtime. +@export var stationary: bool = true +@export var spawn_pos := Vector2(0.0, -140.0) +@export var phases: Array[BossPhase] = [] + + +## Index of the phase that matches [param hp_fraction]. Later entries win, so a +## boss at 0.2 hp picks the lowest-threshold phase that still covers it. +func phase_index_for(hp_fraction: float) -> int: + var chosen := 0 + for i in phases.size(): + if hp_fraction <= phases[i].enter_at_hp_fraction: + chosen = i + return chosen diff --git a/src/actors/boss/boss_def.gd.uid b/src/actors/boss/boss_def.gd.uid new file mode 100644 index 0000000..1ec2fc2 --- /dev/null +++ b/src/actors/boss/boss_def.gd.uid @@ -0,0 +1 @@ +uid://cakbi458wdkrr diff --git a/src/actors/boss/boss_phase.gd b/src/actors/boss/boss_phase.gd new file mode 100644 index 0000000..2beb76e --- /dev/null +++ b/src/actors/boss/boss_phase.gd @@ -0,0 +1,18 @@ +class_name BossPhase +extends Resource +## One stage of a boss fight: a looping timeline of emitters, entered when the +## boss drops below [member enter_at_hp_fraction]. + +@export var name: String = "Phase" +## The phase becomes active once hp/max_hp is at or below this. Phases are +## evaluated in array order, so list them from 1.0 downwards. +@export_range(0.0, 1.0, 0.01) var enter_at_hp_fraction: float = 1.0 +## The phase timeline wraps at this many ticks. Emitter [member start_tick] and +## [member end_tick] are relative to the start of each loop. +@export var loop_ticks: int = 600 +## Ticks of no fire after entering the phase, so the transition is readable. +@export var telegraph_ticks: int = 45 +## Damage taken while in this phase is scaled by this. Use < 1.0 for armoured +## phases rather than adding hit points, so the fight length stays predictable. +@export var damage_taken_mult: float = 1.0 +@export var emitters: Array[BulletEmitter] = [] diff --git a/src/actors/boss/boss_phase.gd.uid b/src/actors/boss/boss_phase.gd.uid new file mode 100644 index 0000000..0a3de5c --- /dev/null +++ b/src/actors/boss/boss_phase.gd.uid @@ -0,0 +1 @@ +uid://dfub6sufpvw1n diff --git a/src/actors/enemies/enemy_def.gd b/src/actors/enemies/enemy_def.gd new file mode 100644 index 0000000..a5bd20f --- /dev/null +++ b/src/actors/enemies/enemy_def.gd @@ -0,0 +1,36 @@ +class_name EnemyDef +extends Resource +## Data-only description of a trash enemy. Behaviour is picked from a small set +## of readable, predictable movements -- players should be able to learn an +## enemy in one encounter. + +enum Move { + ## Never moves. Pure turret. + STATIC, + ## Slides along a fixed heading, bouncing off the arena edges. + DRIFT, + ## Circles its spawn point at a fixed radius. + ORBIT, + ## Walks straight at the nearest player, slowly. + APPROACH, + ## Keeps a preferred distance from the nearest player. + STRAFE, +} + +@export var id: StringName = &"drone" +@export var display_name: String = "Drone" +@export var max_hp: int = 40 +@export var radius: float = 14.0 +@export var contact_damage: int = 10 +@export var move: Move = Move.DRIFT +@export var speed: float = 60.0 +## ORBIT radius, or STRAFE preferred distance. +@export var move_param: float = 120.0 +## Ticks between direction re-evaluations for APPROACH/STRAFE, so enemies read +## as deliberate rather than twitchy. +@export var retarget_interval: int = 30 +## Index the client renderer uses to pick a shape/colour. +@export var visual: int = 0 +@export var emitters: Array[BulletEmitter] = [] +## The emitter timeline wraps at this many ticks. +@export var pattern_loop_ticks: int = 240 diff --git a/src/actors/enemies/enemy_def.gd.uid b/src/actors/enemies/enemy_def.gd.uid new file mode 100644 index 0000000..0f79cb8 --- /dev/null +++ b/src/actors/enemies/enemy_def.gd.uid @@ -0,0 +1 @@ +uid://njtjcrwckton diff --git a/src/autoload/net.gd b/src/autoload/net.gd new file mode 100644 index 0000000..5aa8180 --- /dev/null +++ b/src/autoload/net.gd @@ -0,0 +1,249 @@ +extends Node +## Transport and RPC surface. This is the only autoload in the project. +## +## It has to be an autoload because Godot routes an RPC by node path: the sender +## and receiver must agree on where the node lives, and `/root/Net` is the one +## path that exists identically on a dedicated server and on every client. +## +## The naming rule here is load-bearing: `c_*` is client -> server, `s_*` is +## server -> client. The `s_*` methods are annotated "authority", so the engine +## itself drops any attempt by a client to fake one. + +signal state_changed(new_state: State) + +enum State { OFFLINE, CONNECTING, ONLINE, FAILED } + +## Peer id the engine gives the host. On a listen server this is also a real +## player, which is why every send goes through the helpers below. +const LOCAL_PEER := 1 + +var state: State = State.OFFLINE +var server: ServerRuntime = null +var client: ClientRuntime = null +var last_error: String = "" + + +func _set_state(s: State) -> void: + state = s + state_changed.emit(s) + + +# --- Lifecycle -------------------------------------------------------------- + +func host(port: int) -> Error: + shutdown() + var peer := ENetMultiplayerPeer.new() + var err := peer.create_server(port, Protocol.MAX_CLIENTS, Protocol.CHANNEL_COUNT) + if err != OK: + last_error = "could not bind port %d (error %d)" % [port, err] + GameLog.error("net", last_error) + _set_state(State.FAILED) + return err + multiplayer.multiplayer_peer = peer + multiplayer.peer_connected.connect(_on_peer_connected) + multiplayer.peer_disconnected.connect(_on_peer_disconnected) + server = ServerRuntime.new() + server.name = "Server" + add_child(server) + _set_state(State.ONLINE) + GameLog.info("net", "listening on %d" % port) + return OK + + +func join(address: String, port: int) -> Error: + shutdown() + var peer := ENetMultiplayerPeer.new() + var err := peer.create_client(address, port, Protocol.CHANNEL_COUNT) + if err != OK: + last_error = "could not reach %s:%d (error %d)" % [address, port, err] + GameLog.error("net", last_error) + _set_state(State.FAILED) + return err + multiplayer.multiplayer_peer = peer + multiplayer.connected_to_server.connect(_on_connected) + multiplayer.connection_failed.connect(_on_connect_failed) + multiplayer.server_disconnected.connect(_on_server_disconnected) + client = ClientRuntime.new() + client.name = "Client" + add_child(client) + # Nothing is simulated until the server says hello back; on_welcome turns + # the physics process on. + client.set_physics_process(false) + _set_state(State.CONNECTING) + GameLog.info("net", "connecting to %s:%d" % [address, port]) + return OK + + +func shutdown() -> void: + if multiplayer.multiplayer_peer != null \ + and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer: + multiplayer.multiplayer_peer.close() + multiplayer.multiplayer_peer = null + for sig in [multiplayer.peer_connected, multiplayer.peer_disconnected, + multiplayer.connected_to_server, multiplayer.connection_failed, + multiplayer.server_disconnected]: + for c in sig.get_connections(): + sig.disconnect(c["callable"]) + if server != null: + server.queue_free() + server = null + if client != null: + client.queue_free() + client = null + _set_state(State.OFFLINE) + + +func kick(peer_id: int) -> void: + if multiplayer.multiplayer_peer is ENetMultiplayerPeer: + (multiplayer.multiplayer_peer as ENetMultiplayerPeer).disconnect_peer(peer_id) + + +func is_server() -> bool: + return server != null + + +# --- Transport signals ------------------------------------------------------ + +func _on_peer_connected(peer_id: int) -> void: + if server != null: + server.on_peer_connected(peer_id) + + +func _on_peer_disconnected(peer_id: int) -> void: + if server != null: + server.on_peer_disconnected(peer_id) + + +func _on_connected() -> void: + _set_state(State.ONLINE) + c_hello.rpc_id(1, Protocol.VERSION, GameOpts.player_name) + + +func _on_connect_failed() -> void: + last_error = "connection refused" + GameLog.error("net", last_error) + _set_state(State.FAILED) + + +func _on_server_disconnected() -> void: + last_error = "server closed the connection" + GameLog.warn("net", last_error) + shutdown() + + +## Start a client inside the hosting process. The result is a listen server: +## the host plays through exactly the same code path as a remote player, sending +## input and learning outcomes from snapshots, with the transport short-circuited +## to a function call. No simulation code knows the difference. +func start_local_client() -> void: + if server == null: + GameLog.error("net", "start_local_client called with no server") + return + client = ClientRuntime.new() + client.name = "Client" + add_child(client) + server.on_hello(LOCAL_PEER, Protocol.VERSION, GameOpts.player_name) + + +func _is_local(peer_id: int) -> bool: + return peer_id == LOCAL_PEER and client != null + + +# --- Send helpers ----------------------------------------------------------- +# Every server -> client message goes through one of these rather than calling +# rpc_id directly, so the listen server's own player is reachable too. + +func send_welcome(peer_id: int) -> void: + if _is_local(peer_id): + client.on_welcome(peer_id) + else: + s_welcome.rpc_id(peer_id, peer_id, Protocol.VERSION) + + +func send_enter_instance(peer_id: int, id: int, kind: int, server_tick: int, + boss_id: String, spawn: Vector2) -> void: + if _is_local(peer_id): + client.on_enter_instance(id, kind, server_tick, boss_id, spawn) + else: + s_enter_instance.rpc_id(peer_id, id, kind, server_tick, boss_id, spawn) + + +func send_snapshot(peer_id: int, data: PackedByteArray) -> void: + if _is_local(peer_id): + client.on_snapshot(data) + else: + s_snapshot.rpc_id(peer_id, data) + + +func send_events(peer_id: int, data: PackedByteArray) -> void: + if _is_local(peer_id): + client.on_events(data) + else: + s_events.rpc_id(peer_id, data) + + +func send_reject(peer_id: int, reason: String) -> void: + if _is_local(peer_id): + GameLog.error("net", "local client rejected: %s" % reason) + else: + s_reject.rpc_id(peer_id, reason) + + +func send_input(data: PackedByteArray) -> void: + if server != null: + server.on_input(LOCAL_PEER, data) # listen server: no transport at all + elif state == State.ONLINE and client != null: + c_input.rpc_id(1, data) + + +# --- Client -> server ------------------------------------------------------- + +@rpc("any_peer", "call_remote", "reliable", 1) +func c_hello(version: int, display_name: String) -> void: + if server == null: + return + server.on_hello(multiplayer.get_remote_sender_id(), version, display_name) + + +@rpc("any_peer", "call_remote", "unreliable_ordered", 4) +func c_input(data: PackedByteArray) -> void: + if server == null: + return + server.on_input(multiplayer.get_remote_sender_id(), data) + + +# --- Server -> client ------------------------------------------------------- + +@rpc("authority", "call_remote", "reliable", 1) +func s_welcome(peer_id: int, _version: int) -> void: + if client == null: + return + client.on_welcome(peer_id) + client.set_physics_process(true) + + +@rpc("authority", "call_remote", "reliable", 1) +func s_enter_instance(id: int, kind: int, server_tick: int, boss_id: String, + spawn: Vector2) -> void: + if client == null: + return + client.on_enter_instance(id, kind, server_tick, boss_id, spawn) + + +@rpc("authority", "call_remote", "reliable", 1) +func s_reject(reason: String) -> void: + last_error = reason + GameLog.error("net", "rejected by server: %s" % reason) + _set_state(State.FAILED) + + +@rpc("authority", "call_remote", "unreliable", 2) +func s_snapshot(data: PackedByteArray) -> void: + if client != null: + client.on_snapshot(data) + + +@rpc("authority", "call_remote", "reliable", 3) +func s_events(data: PackedByteArray) -> void: + if client != null: + client.on_events(data) diff --git a/src/autoload/net.gd.uid b/src/autoload/net.gd.uid new file mode 100644 index 0000000..dca7815 --- /dev/null +++ b/src/autoload/net.gd.uid @@ -0,0 +1 @@ +uid://8itnd1lucfeu diff --git a/src/content/content.gd b/src/content/content.gd new file mode 100644 index 0000000..a95cfe1 --- /dev/null +++ b/src/content/content.gd @@ -0,0 +1,299 @@ +class_name Content +extends RefCounted +## All game content, built in code. +## +## Content is GDScript rather than .tres because it is the shape that reviews +## well: a boss is a readable diff, there are no resource UIDs to churn, and a +## test can build one without touching the filesystem. `tools/export_content.gd` +## writes .tres copies into res://resources/ for anyone who prefers to tune +## numbers in the editor inspector -- but this file stays the source of truth. + +const ENEMY_DRIFTER := &"drifter" +const ENEMY_TURRET := &"turret" +const ENEMY_STALKER := &"stalker" +const ENEMY_DUMMY := &"dummy" +const BOSS_WARDEN := &"warden" + + +static func enemy(id: StringName) -> EnemyDef: + match id: + ENEMY_DRIFTER: return drifter() + ENEMY_TURRET: return turret() + ENEMY_STALKER: return stalker() + ENEMY_DUMMY: return dummy() + push_error("unknown enemy id: %s" % id) + return drifter() + + +static func boss(id: StringName) -> BossDef: + match id: + BOSS_WARDEN: return warden() + push_error("unknown boss id: %s" % id) + return warden() + + +# --- Enemies ---------------------------------------------------------------- + +## Reads as: floats across the arena, lobs a slow three-shot fan at you. +static func drifter() -> EnemyDef: + var d := EnemyDef.new() + d.id = ENEMY_DRIFTER + d.display_name = "Drifter" + d.max_hp = 40 + d.radius = 14.0 + d.contact_damage = 8 + d.move = EnemyDef.Move.DRIFT + d.speed = 55.0 + d.visual = 0 + d.pattern_loop_ticks = 120 + + var fan := AimedSpreadEmitter.new() + fan.start_tick = 0 + fan.interval = 120 + fan.count = 3 + fan.spread_deg = 20.0 + fan.speed = 150.0 + fan.radius = 6.0 + fan.damage = 10 + fan.lifetime = 240 + fan.kind = SimConfig.KIND_ORB + d.emitters = [fan] + return d + + +## Never moves, fires a wide ring on a slow beat. Punishes crowding. +static func turret() -> EnemyDef: + var d := EnemyDef.new() + d.id = ENEMY_TURRET + d.display_name = "Turret" + d.max_hp = 70 + d.radius = 16.0 + d.contact_damage = 0 + d.move = EnemyDef.Move.STATIC + d.speed = 0.0 + d.visual = 1 + d.pattern_loop_ticks = 150 + + var ring := RingEmitter.new() + ring.start_tick = 0 + ring.interval = 150 + ring.count = 10 + ring.spin_per_shot_deg = 18.0 + ring.speed = 130.0 + ring.radius = 7.0 + ring.damage = 12 + ring.lifetime = 300 + ring.kind = SimConfig.KIND_ORB + d.emitters = [ring] + return d + + +## Walks at you and does nothing else. The pressure enemy. +static func stalker() -> EnemyDef: + var d := EnemyDef.new() + d.id = ENEMY_STALKER + d.display_name = "Stalker" + d.max_hp = 30 + d.radius = 12.0 + d.contact_damage = 16 + d.move = EnemyDef.Move.APPROACH + d.speed = 95.0 + d.retarget_interval = 20 + d.visual = 2 + return d + + +## Lobby target dummy: inert, tough, so players can feel out the gun. +static func dummy() -> EnemyDef: + var d := EnemyDef.new() + d.id = ENEMY_DUMMY + d.display_name = "Target Dummy" + d.max_hp = 100000 + d.radius = 20.0 + d.contact_damage = 0 + d.move = EnemyDef.Move.STATIC + d.visual = 3 + return d + + +# --- Boss ------------------------------------------------------------------- + +## The Warden of the Fold: stationary, four phases, each layering one more idea +## on the last. Adding a second boss means writing another function like this +## one -- the simulation has no per-boss branches. +static func warden() -> BossDef: + var b := BossDef.new() + b.id = BOSS_WARDEN + b.display_name = "Warden of the Fold" + b.max_hp = 3600 + b.radius = 42.0 + b.stationary = true + b.spawn_pos = Vector2(0.0, -150.0) + b.phases = [_warden_p1(), _warden_p2(), _warden_p3(), _warden_p4()] + return b + + +## Phase 1 -- one idea at a time: a lazily spinning ring you walk around, with +## an aimed fan that stops you from standing still. +static func _warden_p1() -> BossPhase: + var p := BossPhase.new() + p.name = "Opening Fold" + p.enter_at_hp_fraction = 1.0 + p.loop_ticks = 480 + p.telegraph_ticks = 60 + + var ring := RingEmitter.new() + ring.interval = 45 + ring.count = 14 + ring.spin_per_shot_deg = 9.0 + ring.speed = 135.0 + ring.radius = 8.0 + ring.damage = 14 + ring.lifetime = 420 + ring.muzzle_offset = 46.0 + + var fan := AimedSpreadEmitter.new() + fan.start_tick = 120 + fan.interval = 150 + fan.count = 5 + fan.spread_deg = 26.0 + fan.speed = 210.0 + fan.radius = 6.0 + fan.damage = 12 + fan.kind = SimConfig.KIND_NEEDLE + fan.muzzle_offset = 46.0 + + p.emitters = [ring, fan] + return p + + +## Phase 2 -- adds a moving safe lane: the spiral fills space while a wall with +## a sliding gap forces a committed dodge. +static func _warden_p2() -> BossPhase: + var p := BossPhase.new() + p.name = "Sliding Lattice" + p.enter_at_hp_fraction = 0.72 + p.loop_ticks = 600 + p.telegraph_ticks = 50 + + var spiral := RingEmitter.new() + spiral.interval = 9 + spiral.count = 4 + spiral.spin_per_shot_deg = 23.0 + spiral.speed = 120.0 + spiral.radius = 7.0 + spiral.damage = 12 + spiral.lifetime = 480 + spiral.muzzle_offset = 46.0 + + var wall := WallGapEmitter.new() + wall.start_tick = 90 + wall.interval = 170 + wall.direction = 0 + wall.count = 20 + wall.gap_width = 3 + wall.gap_step = 7 + wall.speed = 175.0 + wall.radius = 8.0 + wall.damage = 16 + wall.lifetime = 300 + wall.kind = SimConfig.KIND_HEAVY + + p.emitters = [spiral, wall] + return p + + +## Phase 3 -- adds rotation you have to track: two sweeping arms that reverse, +## plus jittered aimed fire so the safe pocket is never exactly where it was. +static func _warden_p3() -> BossPhase: + var p := BossPhase.new() + p.name = "Reaping Arms" + p.enter_at_hp_fraction = 0.42 + p.loop_ticks = 720 + p.telegraph_ticks = 45 + p.damage_taken_mult = 1.15 + + var arms := ArcSweepEmitter.new() + arms.interval = 5 + arms.arms = 3 + arms.bullets_per_arm = 2 + arms.arm_spacing = 26.0 + arms.sweep_deg = 95.0 + arms.sweep_period = 5.0 + arms.speed = 165.0 + arms.radius = 7.0 + arms.damage = 13 + arms.lifetime = 400 + + var fan := AimedSpreadEmitter.new() + fan.start_tick = 60 + fan.interval = 110 + fan.count = 7 + fan.spread_deg = 40.0 + fan.jitter_deg = 3.0 + fan.speed = 195.0 + fan.edge_speed_bonus = 0.25 + fan.radius = 6.0 + fan.damage = 12 + fan.kind = SimConfig.KIND_NEEDLE + + var counter_ring := RingEmitter.new() + counter_ring.start_tick = 240 + counter_ring.end_tick = 600 + counter_ring.interval = 60 + counter_ring.count = 18 + counter_ring.spin_per_shot_deg = -14.0 + counter_ring.speed = 105.0 + counter_ring.radius = 7.0 + counter_ring.damage = 12 + counter_ring.lifetime = 420 + + p.emitters = [arms, fan, counter_ring] + return p + + +## Phase 4 -- everything at once, plus curving bullets that close the pockets +## the earlier phases taught you to use. +static func _warden_p4() -> BossPhase: + var p := BossPhase.new() + p.name = "Collapse" + p.enter_at_hp_fraction = 0.16 + p.loop_ticks = 420 + p.telegraph_ticks = 60 + p.damage_taken_mult = 1.3 + + var curve := RingEmitter.new() + curve.interval = 11 + curve.count = 5 + curve.spin_per_shot_deg = 31.0 + curve.speed = 130.0 + curve.turn_deg = 0.55 + curve.radius = 7.0 + curve.damage = 15 + curve.lifetime = 400 + curve.muzzle_offset = 46.0 + + var walls := WallGapEmitter.new() + walls.start_tick = 40 + walls.interval = 130 + walls.direction = 2 + walls.count = 16 + walls.gap_width = 3 + walls.gap_step = 5 + walls.speed = 200.0 + walls.radius = 8.0 + walls.damage = 18 + walls.kind = SimConfig.KIND_HEAVY + + var snipe := AimedSpreadEmitter.new() + snipe.start_tick = 100 + snipe.interval = 80 + snipe.count = 3 + snipe.spread_deg = 10.0 + snipe.speed = 300.0 + snipe.radius = 5.0 + snipe.damage = 16 + snipe.kind = SimConfig.KIND_NEEDLE + + p.emitters = [curve, walls, snipe] + return p diff --git a/src/content/content.gd.uid b/src/content/content.gd.uid new file mode 100644 index 0000000..4bbc26a --- /dev/null +++ b/src/content/content.gd.uid @@ -0,0 +1 @@ +uid://dv03hinp1g3ce diff --git a/src/core/game_log.gd b/src/core/game_log.gd new file mode 100644 index 0000000..34a1fda --- /dev/null +++ b/src/core/game_log.gd @@ -0,0 +1,37 @@ +class_name GameLog +extends RefCounted +## Tagged logging that survives headless runs. +## +## Static rather than an autoload so it works from unit tests and from +## `--script` tool runs, where autoload identifiers are not registered at all. +## The dedicated server has no console UI, so every line carries the role that +## produced it -- that is what makes a captured server log readable later. + +enum Level { DEBUG, INFO, WARN, ERROR } + +## Raise to WARN on a busy server; DEBUG is very chatty at 60 Hz. +static var min_level: Level = Level.INFO +static var role_tag: String = "?" + + +static func _fmt(level: Level, tag: String, msg: String) -> String: + return "[%s][%s][%s] %s" % [Level.keys()[level], role_tag, tag, msg] + + +static func debug(tag: String, msg: String) -> void: + if min_level <= Level.DEBUG: + print(_fmt(Level.DEBUG, tag, msg)) + + +static func info(tag: String, msg: String) -> void: + if min_level <= Level.INFO: + print(_fmt(Level.INFO, tag, msg)) + + +static func warn(tag: String, msg: String) -> void: + if min_level <= Level.WARN: + print(_fmt(Level.WARN, tag, msg)) + + +static func error(tag: String, msg: String) -> void: + printerr(_fmt(Level.ERROR, tag, msg)) diff --git a/src/core/game_log.gd.uid b/src/core/game_log.gd.uid new file mode 100644 index 0000000..a0487da --- /dev/null +++ b/src/core/game_log.gd.uid @@ -0,0 +1 @@ +uid://dlmfc7pqn6p4s diff --git a/src/core/game_opts.gd b/src/core/game_opts.gd new file mode 100644 index 0000000..ba54271 --- /dev/null +++ b/src/core/game_opts.gd @@ -0,0 +1,67 @@ +class_name GameOpts +extends RefCounted +## Process-wide options parsed from the command line. +## +## These are per-machine settings and must never affect the simulation -- if a +## value would change how the world evolves, it belongs in [SimConfig] so server +## and client cannot disagree about it. + +static var is_server: bool = false +static var port: int = Protocol.DEFAULT_PORT +static var host_address: String = "127.0.0.1" +static var player_name: String = "player" +## Replaces keyboard input with a scripted loop so CI can play the game with no +## display. See `tools/smoke.sh`. +static var bot_client: bool = false +## Quit after this many physics ticks. 0 = run forever. +static var autoquit_ticks: int = 0 +## Skip the menu and connect straight away. Implied by --bot. +static var autojoin: bool = false +## Skip the menu and start a listen server (host + local player). +static var listen: bool = false +## Server-side dev switch: new dungeons open straight onto the boss, skipping +## the trash waves. For iterating on a pattern without clearing two waves first. +static var boss_rush: bool = false +static var parsed: bool = false + + +static func parse(argv: PackedStringArray = PackedStringArray()) -> void: + if argv.is_empty(): + argv = OS.get_cmdline_user_args() + var i := 0 + while i < argv.size(): + match argv[i]: + "--server": + is_server = true + "--bot": + bot_client = true + autojoin = true + "--join": + autojoin = true + "--listen": + listen = true + "--boss-rush": + boss_rush = true + "--port": + i += 1 + if i < argv.size(): + port = int(argv[i]) + "--host": + i += 1 + if i < argv.size(): + host_address = argv[i] + "--name": + i += 1 + if i < argv.size(): + player_name = argv[i] + "--autoquit": + i += 1 + if i < argv.size(): + autoquit_ticks = int(argv[i]) + "--verbose": + GameLog.min_level = GameLog.Level.DEBUG + "--quiet": + GameLog.min_level = GameLog.Level.WARN + i += 1 + GameLog.role_tag = "server" if is_server else "client" + parsed = true diff --git a/src/core/game_opts.gd.uid b/src/core/game_opts.gd.uid new file mode 100644 index 0000000..839dfee --- /dev/null +++ b/src/core/game_opts.gd.uid @@ -0,0 +1 @@ +uid://cneth7xd6rb68 diff --git a/src/core/movement.gd b/src/core/movement.gd new file mode 100644 index 0000000..92607de --- /dev/null +++ b/src/core/movement.gd @@ -0,0 +1,31 @@ +class_name Movement +extends RefCounted +## Pure movement helpers. These run on the server (authoritative) and on the +## client (prediction). They must stay side-effect free so a client can replay +## them over a history of inputs and land on the same result as the server. + +## Clamp a raw analogue/keyboard vector to a unit disc. +static func sanitize_move(raw: Vector2) -> Vector2: + if raw.length_squared() > 1.0: + return raw.normalized() + return raw + + +## One tick of player locomotion. Returns the new position. +static func step_player(pos: Vector2, move: Vector2, speed: float, bounds: Vector2) -> Vector2: + var next := pos + sanitize_move(move) * speed * SimConfig.TICK_DELTA + next.x = clampf(next.x, -bounds.x, bounds.x) + next.y = clampf(next.y, -bounds.y, bounds.y) + return next + + +## Circle-vs-circle overlap test used for every hit in the game. +static func circles_overlap(a: Vector2, ar: float, b: Vector2, br: float) -> bool: + var r := ar + br + return a.distance_squared_to(b) <= r * r + + +## True when a point has drifted outside the arena plus the cull margin. +static func outside_arena(p: Vector2, margin: float = SimConfig.BULLET_CULL_MARGIN) -> bool: + return absf(p.x) > SimConfig.ARENA_HALF.x + margin \ + or absf(p.y) > SimConfig.ARENA_HALF.y + margin diff --git a/src/core/movement.gd.uid b/src/core/movement.gd.uid new file mode 100644 index 0000000..27aa9b7 --- /dev/null +++ b/src/core/movement.gd.uid @@ -0,0 +1 @@ +uid://upjfa2xdgjct diff --git a/src/core/sim_config.gd b/src/core/sim_config.gd new file mode 100644 index 0000000..f53480f --- /dev/null +++ b/src/core/sim_config.gd @@ -0,0 +1,64 @@ +class_name SimConfig +extends RefCounted +## Tuning constants shared verbatim by the server simulation and the client +## replica. Nothing here may differ between the two builds -- if a value needs +## to differ, it belongs in [GameConfig], not here. + +# --- Time ------------------------------------------------------------------- +const TICK_RATE := 60 +const TICK_DELTA := 1.0 / 60.0 +## Server sends a snapshot every N ticks (60 / 3 = 20 Hz). +const SNAPSHOT_INTERVAL := 3 +## How far behind the newest snapshot the client renders remote actors. +const INTERPOLATION_DELAY_TICKS := 6 + +# --- Arena ------------------------------------------------------------------ +const ARENA_HALF := Vector2(620.0, 340.0) +## Bullets are culled once they leave the arena by this margin. +const BULLET_CULL_MARGIN := 64.0 + +# --- Player ----------------------------------------------------------------- +const PLAYER_SPEED := 240.0 +const PLAYER_RADIUS := 9.0 +const PLAYER_MAX_HP := 100 +const PLAYER_FIRE_COOLDOWN := 7 # ticks +const PLAYER_BULLET_SPEED := 620.0 +const PLAYER_BULLET_RADIUS := 4.0 +const PLAYER_BULLET_LIFETIME := 90 # ticks +const PLAYER_BULLET_DAMAGE := 6 +const PLAYER_IFRAMES := 36 # ticks of invulnerability after a hit +const PLAYER_RESPAWN_DELAY := 180 # ticks + +# --- Anti-cheat guards ------------------------------------------------------ +## Inputs older than this (relative to the newest accepted) are discarded. +const INPUT_MAX_AGE := 30 +## Inputs claiming to be further ahead than this of the server tick are clamped. +const INPUT_MAX_LEAD := 12 +## Hard ceiling on inputs consumed from one peer in a single tick. +const INPUT_MAX_PER_TICK := 4 + +# --- Emergency escape ------------------------------------------------------- +const ESCAPE_CHANNEL_TICKS := 180 # 3 seconds +## Taking damage while channelling cancels the escape. +const ESCAPE_BREAK_ON_DAMAGE := true + +# --- Bullets ---------------------------------------------------------------- +const MAX_BULLETS := 4096 +const TEAM_PLAYER := 0 +const TEAM_ENEMY := 1 + +# --- Bullet visual kinds (index into the renderer's atlas) ------------------ +const KIND_PLAYER_SHOT := 0 +const KIND_ORB := 1 +const KIND_NEEDLE := 2 +const KIND_HEAVY := 3 + +# --- Instances -------------------------------------------------------------- +const LOBBY_INSTANCE_ID := 1 +const DUNGEON_PARTY_MAX := 4 +## How long a forming dungeon waits for more players before it locks. +const DUNGEON_FORMING_TICKS := 300 + +# --- Portal ----------------------------------------------------------------- +const PORTAL_POS := Vector2(0.0, -220.0) +const PORTAL_RADIUS := 60.0 diff --git a/src/core/sim_config.gd.uid b/src/core/sim_config.gd.uid new file mode 100644 index 0000000..47e02f1 --- /dev/null +++ b/src/core/sim_config.gd.uid @@ -0,0 +1 @@ +uid://msxyswmkdty1 diff --git a/src/game.tscn b/src/game.tscn new file mode 100644 index 0000000..b32f9cf --- /dev/null +++ b/src/game.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://src/view/game_scene.gd" id="1"] +[ext_resource type="Script" path="res://src/view/world_view.gd" id="2"] +[ext_resource type="Script" path="res://src/view/bullet_renderer.gd" id="3"] +[ext_resource type="Script" path="res://src/ui/hud.gd" id="4"] + +[node name="Game" type="Node2D"] +script = ExtResource("1") + +[node name="WorldView" type="Node2D" parent="."] +position = Vector2(640, 360) +script = ExtResource("2") + +[node name="Bullets" type="MultiMeshInstance2D" parent="WorldView"] +script = ExtResource("3") + +[node name="HUD" type="CanvasLayer" parent="."] +script = ExtResource("4") diff --git a/src/instances/instance.gd b/src/instances/instance.gd new file mode 100644 index 0000000..8cbd1bf --- /dev/null +++ b/src/instances/instance.gd @@ -0,0 +1,147 @@ +class_name Instance +extends RefCounted +## One live world on the server: the shared lobby hub, or one dungeon run for a +## party. Instances are cheap -- a [SimWorld] and a peer list -- so a single +## server process hosts the hub plus every concurrent dungeon. + +enum State { + ## Dungeon is open for players to join from the hub. + FORMING, + ## Fight in progress. + ACTIVE, + ## Boss dead; players are being returned to the hub. + CLEARED, +} + +var id: int = 0 +var kind: Protocol.InstanceKind = Protocol.InstanceKind.LOBBY +var world: SimWorld +var peers: Array[int] = [] +var state: State = State.ACTIVE +var age: int = 0 +var seed_value: int = 0 + +## Dungeon progression. -1 is the pre-fight breather. +var stage: int = -1 +var stage_delay: int = 0 +var boss_id: StringName = &"" + + +static func make_lobby(instance_id: int) -> Instance: + var inst := Instance.new() + inst.id = instance_id + inst.kind = Protocol.InstanceKind.LOBBY + inst.seed_value = 1 + inst.world = SimWorld.new(inst.seed_value) + inst.world.portal_enabled = true + inst.world.spawn_point = Vector2(0.0, 120.0) + inst.state = State.ACTIVE + # A single inert dummy so players can feel out the gun before committing to + # a run. It has no emitters and no contact damage. + inst.world.spawn_enemy(Content.dummy(), Vector2(-220.0, -40.0)) + return inst + + +static func make_dungeon(instance_id: int, dungeon_seed: int) -> Instance: + var inst := Instance.new() + inst.id = instance_id + inst.kind = Protocol.InstanceKind.DUNGEON + inst.seed_value = dungeon_seed + inst.world = SimWorld.new(dungeon_seed) + inst.world.spawn_point = Vector2(0.0, 260.0) + inst.boss_id = Content.BOSS_WARDEN + inst.state = State.FORMING + if GameOpts.boss_rush: + # Dev switch: the next stage advance lands on the boss. + inst.stage = 1 + return inst + + +func add_peer(peer_id: int, display_name: String) -> void: + if not peers.has(peer_id): + peers.append(peer_id) + world.add_player(peer_id, display_name) + + +func remove_peer(peer_id: int) -> void: + peers.erase(peer_id) + world.remove_player(peer_id) + + +func is_empty() -> bool: + return peers.is_empty() + + +func accepts_new_party_member() -> bool: + return kind == Protocol.InstanceKind.DUNGEON \ + and state == State.FORMING \ + and peers.size() < SimConfig.DUNGEON_PARTY_MAX + + +func step() -> void: + age += 1 + world.step() + if kind == Protocol.InstanceKind.DUNGEON: + _step_dungeon() + + +## Straight-line progression: two trash waves, then the boss. Waves gate on +## "everything dead" rather than a timer so a slow party is never overrun. +func _step_dungeon() -> void: + # The delay has to tick down before the CLEARED check, or a cleared dungeon + # would sit on its exit timer forever and never release its party. + if stage_delay > 0: + stage_delay -= 1 + return + if state == State.CLEARED: + return + + if state == State.FORMING: + if age >= SimConfig.DUNGEON_FORMING_TICKS or peers.size() >= SimConfig.DUNGEON_PARTY_MAX: + state = State.ACTIVE + # stage is left as make_dungeon set it -- resetting it here would + # silently undo the --boss-rush dev switch. + stage_delay = 90 + return + + if stage >= 0 and _live_enemy_count() > 0: + return + if stage == 2: + if world.boss != null and world.boss.alive: + return + state = State.CLEARED + stage_delay = 300 + return + + stage += 1 + stage_delay = 60 + match stage: + 0: _spawn_wave_one() + 1: _spawn_wave_two() + 2: + world.spawn_boss(Content.boss(boss_id)) + GameLog.info("instance", "BOSS_SPAWNED %s in instance %d" % [boss_id, id]) + GameLog.debug("instance", "instance %d entered stage %d" % [id, stage]) + + +func _live_enemy_count() -> int: + var n := 0 + for e in world.enemies.values(): + if e.alive: + n += 1 + return n + + +func _spawn_wave_one() -> void: + for i in 3: + world.spawn_enemy(Content.drifter(), Vector2(-320.0 + 320.0 * float(i), -120.0), i * 40) + world.spawn_enemy(Content.turret(), Vector2(-420.0, -220.0), 0) + world.spawn_enemy(Content.turret(), Vector2(420.0, -220.0), 75) + + +func _spawn_wave_two() -> void: + for i in 4: + world.spawn_enemy(Content.stalker(), Vector2(-300.0 + 200.0 * float(i), -260.0), i * 15) + for i in 3: + world.spawn_enemy(Content.turret(), Vector2(-380.0 + 380.0 * float(i), -60.0), i * 50) + world.spawn_enemy(Content.drifter(), Vector2(0.0, -300.0), 20) diff --git a/src/instances/instance.gd.uid b/src/instances/instance.gd.uid new file mode 100644 index 0000000..3d61a29 --- /dev/null +++ b/src/instances/instance.gd.uid @@ -0,0 +1 @@ +uid://bbdqtks6mqgr1 diff --git a/src/main.gd b/src/main.gd new file mode 100644 index 0000000..797c9f0 --- /dev/null +++ b/src/main.gd @@ -0,0 +1,112 @@ +extends Node +## Process entry point. One executable is both the dedicated server and the +## client -- which one it becomes is decided here from the command line, so a +## server build is just `--headless -- --server` rather than a separate export. + +const GAME_SCENE := preload("res://src/game.tscn") + +var _menu: Control = null +var _game: Node = null +var _ticks: int = 0 + + +func _ready() -> void: + GameOpts.parse() + # The simulation is written against a fixed 60 Hz tick. Assert it in code + # rather than trusting project.godot, which drops settings equal to the + # engine default and would silently desync server and client if that + # default ever changed. + Engine.physics_ticks_per_second = SimConfig.TICK_RATE + Net.state_changed.connect(_on_net_state) + + if GameOpts.is_server: + _run_dedicated_server() + elif GameOpts.listen: + _host_and_play(GameOpts.port) + elif GameOpts.autojoin: + _join(GameOpts.host_address, GameOpts.port) + else: + _show_menu() + + +func _physics_process(_delta: float) -> void: + _ticks += 1 + if GameOpts.autoquit_ticks > 0 and _ticks >= GameOpts.autoquit_ticks: + GameLog.info("main", "autoquit after %d ticks" % _ticks) + Net.shutdown() + get_tree().quit(0) + + +func _run_dedicated_server() -> void: + if Net.host(GameOpts.port) != OK: + GameLog.error("main", "server failed to start: %s" % Net.last_error) + get_tree().quit(1) + return + # A dedicated server has no view, and headless mode has no display to draw + # to. Leaving the scene empty keeps the process at simulation cost only. + # Nothing to render, so do not let the main loop spin faster than the tick. + Engine.max_fps = SimConfig.TICK_RATE + GameLog.info("main", "SERVER_READY port=%d" % GameOpts.port) + + +func _show_menu() -> void: + if _menu != null: + return + _clear_game() + _menu = preload("res://src/ui/main_menu.gd").new() + _menu.join_requested.connect(_join) + _menu.host_requested.connect(_host_and_play) + add_child(_menu) + + +func _join(address: String, port: int) -> void: + GameOpts.host_address = address + GameOpts.port = port + if Net.join(address, port) != OK: + _menu_status(Net.last_error) + return + _enter_game() + + +## Listen server: one process runs the authoritative world and a local client +## connected over loopback. The client takes no shortcuts because of it -- it +## still only sends input and still only learns outcomes from snapshots. +func _host_and_play(port: int) -> void: + if Net.host(port) != OK: + _menu_status(Net.last_error) + return + GameLog.role_tag = "listen" + Net.start_local_client() + _enter_game() + + +func _enter_game() -> void: + if _menu != null: + _menu.queue_free() + _menu = null + if _game == null: + _game = GAME_SCENE.instantiate() + add_child(_game) + + +func _clear_game() -> void: + if _game != null: + _game.queue_free() + _game = null + + +func _menu_status(text: String) -> void: + if _menu != null: + _menu.set_status(text) + + +func _on_net_state(state: Net.State) -> void: + match state: + Net.State.FAILED: + _show_menu() + _menu_status(Net.last_error) + Net.State.OFFLINE: + if not GameOpts.is_server: + _show_menu() + _: + pass diff --git a/src/main.gd.uid b/src/main.gd.uid new file mode 100644 index 0000000..353c39c --- /dev/null +++ b/src/main.gd.uid @@ -0,0 +1 @@ +uid://4y8xrnuafwd8 diff --git a/src/main.tscn b/src/main.tscn new file mode 100644 index 0000000..e6e0254 --- /dev/null +++ b/src/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://src/main.gd" id="1"] + +[node name="Main" type="Node2D"] +script = ExtResource("1") diff --git a/src/net/client_runtime.gd b/src/net/client_runtime.gd new file mode 100644 index 0000000..54cc3f4 --- /dev/null +++ b/src/net/client_runtime.gd @@ -0,0 +1,252 @@ +class_name ClientRuntime +extends Node +## The client half: sample input, predict the local player, interpolate everyone +## else, and replay the bullets the server told us about. +## +## The client owns exactly one thing -- where it *thinks* the local player is, +## so the stick feels instant. Every consequence (damage, death, escape, loot) +## comes back from the server and overwrites whatever the client believed. + +signal instance_changed +signal hud_dirty +signal local_hit(damage: int) + +var my_peer: int = 0 +var instance_id: int = 0 +var instance_kind: Protocol.InstanceKind = Protocol.InstanceKind.LOBBY +var boss_def: BossDef = null + +## Replica world. [member SimWorld.authoritative] is false, so it integrates +## bullets and nothing else. +var world := SimWorld.new() + +## Local estimate of the server's tick, used to age incoming bullets. +var server_tick_est: int = 0 +var input_tick: int = 0 + +var predicted_pos := Vector2.ZERO +var aim: float = 0.0 +var pending: Array[InputFrame] = [] + +# Authoritative mirror of the local player. +var my_hp: int = SimConfig.PLAYER_MAX_HP +var my_alive: bool = true +var my_escape: float = 0.0 +var my_escaping: bool = false + +var snap_prev: Dictionary = {} +var snap_curr: Dictionary = {} +var _interp: float = 0.0 +var _bot_tick: int = 0 + + +func _ready() -> void: + world.authoritative = false + set_physics_process(true) + + +func _physics_process(delta: float) -> void: + server_tick_est += 1 + input_tick += 1 + + var frame := _sample_input() + pending.append(frame) + # Only enough history to cover the worst reconciliation window. + while pending.size() > SimConfig.INPUT_MAX_AGE: + pending.pop_front() + + if my_alive: + predicted_pos = Movement.step_player(predicted_pos, frame.move, + SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + + # Send the last few frames every tick. Inputs are unreliable-ordered, so the + # redundancy is what covers a dropped packet without a retransmit stall. + var redundant: Array[InputFrame] = [] + var from := maxi(pending.size() - 3, 0) + for i in range(from, pending.size()): + redundant.append(pending[i]) + Net.send_input(NetCodec.encode_inputs(redundant)) + + world.step() + _interp = minf(_interp + delta * float(SimConfig.TICK_RATE) / float(SimConfig.SNAPSHOT_INTERVAL), 1.0) + + +# --- Input ------------------------------------------------------------------ + +func _sample_input() -> InputFrame: + if GameOpts.bot_client: + return _bot_input() + var move := Input.get_vector("move_left", "move_right", "move_up", "move_down") + var mouse := get_viewport().get_mouse_position() - get_viewport().get_visible_rect().size * 0.5 + var to_mouse := mouse - predicted_pos + if to_mouse.length_squared() > 1.0: + aim = to_mouse.angle() + var buttons := 0 + if Input.is_action_pressed("fire"): + buttons |= InputFrame.BTN_FIRE + if Input.is_action_pressed("emergency_escape"): + buttons |= InputFrame.BTN_ESCAPE + if Input.is_action_pressed("interact"): + buttons |= InputFrame.BTN_INTERACT + return InputFrame.make(input_tick, move, aim, buttons) + + +## Scripted input so `tools/smoke.sh` can play the game with no display: orbit +## the arena, fire constantly, take the portal, then punch out with the escape. +func _bot_input() -> InputFrame: + _bot_tick += 1 + var t := float(_bot_tick) * SimConfig.TICK_DELTA + var move := Vector2(cos(t * 0.9), sin(t * 1.3)) + aim = t * 2.1 + var buttons := InputFrame.BTN_FIRE + if instance_kind == Protocol.InstanceKind.LOBBY and _bot_tick % 120 < 30: + buttons |= InputFrame.BTN_INTERACT + # Walk onto the portal instead of orbiting, or interact never lands. + move = (SimConfig.PORTAL_POS - predicted_pos).normalized() + if instance_kind == Protocol.InstanceKind.DUNGEON and _bot_tick > 900: + buttons |= InputFrame.BTN_ESCAPE + return InputFrame.make(input_tick, move, aim, buttons) + + +# --- Server messages -------------------------------------------------------- + +func on_welcome(peer_id: int) -> void: + my_peer = peer_id + set_physics_process(true) + GameLog.info("client", "welcome, peer id %d" % peer_id) + + +func on_enter_instance(id: int, kind: int, server_tick: int, boss_id: String, + spawn: Vector2) -> void: + instance_id = id + instance_kind = kind as Protocol.InstanceKind + boss_def = Content.boss(StringName(boss_id)) if not boss_id.is_empty() else null + world.pool.clear() + snap_prev = {} + snap_curr = {} + predicted_pos = spawn + pending.clear() + server_tick_est = server_tick + input_tick = server_tick + 8 + my_alive = true + my_hp = SimConfig.PLAYER_MAX_HP + my_escape = 0.0 + my_escaping = false + GameLog.info("client", "entered instance %d (%s)" % [id, Protocol.InstanceKind.keys()[kind]]) + instance_changed.emit() + hud_dirty.emit() + + +func on_snapshot(data: PackedByteArray) -> void: + var snap := NetCodec.decode_snapshot(data) + if not snap_curr.is_empty() and int(snap["tick"]) <= int(snap_curr["tick"]): + return # stale or duplicate; unreliable channel, newest wins + snap_prev = snap_curr + snap_curr = snap + _interp = 0.0 + + var tick := int(snap["tick"]) + if server_tick_est < tick or server_tick_est > tick + 12: + server_tick_est = tick + # Keep the client roughly one buffer ahead of the server so inputs arrive + # just before they are needed rather than late. + var lead := input_tick - tick + if lead < 2 or lead > 16: + input_tick = tick + 8 + + for rec: Dictionary in snap["players"]: + if int(rec["peer"]) == my_peer: + _reconcile(rec) + break + + +## Rewind to the server's position, replay every input it has not seen yet, and +## land where the client should actually be right now. +func _reconcile(rec: Dictionary) -> void: + my_hp = int(rec["hp"]) + my_alive = (int(rec["flags"]) & Protocol.F_ALIVE) != 0 + my_escaping = (int(rec["flags"]) & Protocol.F_ESCAPING) != 0 + my_escape = float(rec["escape"]) + hud_dirty.emit() + + var acked := int(rec["last_input_tick"]) + while not pending.is_empty() and pending[0].tick <= acked: + pending.pop_front() + + var p: Vector2 = rec["pos"] + if my_alive: + for f in pending: + p = Movement.step_player(p, f.move, SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + var error := predicted_pos.distance_to(p) + if error > 24.0: + predicted_pos = p # real divergence: take the server's word + elif error > 0.5: + predicted_pos = predicted_pos.lerp(p, 0.3) # smooth out jitter + + +func on_events(data: PackedByteArray) -> void: + var packet := NetCodec.decode_events(data) + var catchup := clampi(server_tick_est - int(packet["tick"]), 0, 30) + for ev: Dictionary in packet["events"]: + match int(ev["t"]): + SimEvent.Type.BULLET_SPAWN: + var slot := world.pool.spawn(ev["pos"], ev["vel"], ev["r"], ev["life"], 0, + ev["team"], ev["kind"], ev["accel"], ev["turn"], ev["uid"]) + if slot >= 0 and catchup > 0: + world.pool.advance_slot(slot, catchup) + SimEvent.Type.BULLET_DESPAWN: + world.apply_event(ev) + SimEvent.Type.PLAYER_HIT: + if int(ev["peer"]) == my_peer: + my_hp = int(ev["hp"]) + local_hit.emit(int(ev["dmg"])) + hud_dirty.emit() + SimEvent.Type.PLAYER_DIED: + if int(ev["peer"]) == my_peer: + my_alive = false + hud_dirty.emit() + SimEvent.Type.PLAYER_RESPAWNED: + if int(ev["peer"]) == my_peer: + my_alive = true + predicted_pos = ev["pos"] + pending.clear() + hud_dirty.emit() + _: + pass + + +# --- View queries ----------------------------------------------------------- + +## Remote players, interpolated between the last two snapshots. The local player +## is excluded -- the view draws [member predicted_pos] for that one. +func remote_players() -> Array[Dictionary]: + return _interpolated("players", "peer", [my_peer]) + + +func enemies() -> Array[Dictionary]: + return _interpolated("enemies", "id", []) + + +func boss_state() -> Dictionary: + if snap_curr.is_empty() or snap_curr.get("boss") == null: + return {} + return snap_curr["boss"] + + +func _interpolated(list_key: String, id_key: String, exclude: Array) -> Array[Dictionary]: + var out: Array[Dictionary] = [] + if snap_curr.is_empty(): + return out + var prev_by_id := {} + if not snap_prev.is_empty(): + for r: Dictionary in snap_prev[list_key]: + prev_by_id[r[id_key]] = r + for r: Dictionary in snap_curr[list_key]: + if exclude.has(r[id_key]): + continue + var rec: Dictionary = r.duplicate() + var old: Variant = prev_by_id.get(r[id_key]) + if old != null: + rec["pos"] = (old["pos"] as Vector2).lerp(r["pos"], _interp) + out.append(rec) + return out diff --git a/src/net/client_runtime.gd.uid b/src/net/client_runtime.gd.uid new file mode 100644 index 0000000..cf4c625 --- /dev/null +++ b/src/net/client_runtime.gd.uid @@ -0,0 +1 @@ +uid://2hlhlktdlix diff --git a/src/net/net_codec.gd b/src/net/net_codec.gd new file mode 100644 index 0000000..e84a213 --- /dev/null +++ b/src/net/net_codec.gd @@ -0,0 +1,234 @@ +class_name NetCodec +extends RefCounted +## Binary encoders for the two server -> client streams. +## +## Snapshots are lossy and unreliable: only what is needed to draw and predict. +## Events are exact and reliable: things a client can never re-derive, above all +## bullet spawns and the despawns caused by a hit. + +# --- Snapshot --------------------------------------------------------------- + +static func encode_snapshot(world: SimWorld) -> PackedByteArray: + var b := StreamPeerBuffer.new() + b.big_endian = false + b.put_u32(world.tick) + + b.put_u8(mini(world.players.size(), 255)) + for p in world.players.values(): + b.put_u32(p.peer_id) + b.put_float(p.pos.x) + b.put_float(p.pos.y) + b.put_u16(wrapi(roundi(p.aim / TAU * 65536.0), 0, 65536)) + b.put_u16(clampi(p.hp, 0, 65535)) + var flags := 0 + if p.alive: + flags |= Protocol.F_ALIVE + if p.iframes > 0: + flags |= Protocol.F_INVULN + if p.escape_ticks > 0: + flags |= Protocol.F_ESCAPING + b.put_u8(flags) + b.put_u8(clampi(roundi(p.escape_progress() * 255.0), 0, 255)) + # Echoed so the owning client knows how far to rewind when reconciling. + b.put_u32(p.last_input_tick) + + var live_enemies: Array[SimEnemy] = [] + for e in world.enemies.values(): + if e.alive: + live_enemies.append(e) + b.put_u16(mini(live_enemies.size(), 65535)) + for e in live_enemies: + b.put_u32(e.id) + b.put_float(e.pos.x) + b.put_float(e.pos.y) + b.put_u16(clampi(e.hp, 0, 65535)) + # Radius and visual travel with the snapshot so a client that joins + # mid-fight can draw an enemy without any extra handshake. + b.put_u8(clampi(roundi(e.def.radius * 2.0), 0, 255)) + b.put_u8(clampi(e.def.visual, 0, 255)) + + var has_boss := world.boss != null and world.boss.alive + b.put_u8(1 if has_boss else 0) + if has_boss: + b.put_u32(world.boss.id) + b.put_float(world.boss.pos.x) + b.put_float(world.boss.pos.y) + b.put_u32(maxi(world.boss.hp, 0)) + b.put_u8(clampi(world.boss.phase_index, 0, 255)) + return b.data_array + + +static func decode_snapshot(data: PackedByteArray) -> Dictionary: + var b := StreamPeerBuffer.new() + b.big_endian = false + b.data_array = data + var snap := {"tick": b.get_u32(), "players": [], "enemies": [], "boss": null} + + var pcount := b.get_u8() + for _i in pcount: + snap["players"].append({ + "peer": b.get_u32(), + "pos": Vector2(b.get_float(), b.get_float()), + "aim": float(b.get_u16()) / 65536.0 * TAU, + "hp": b.get_u16(), + "flags": b.get_u8(), + "escape": float(b.get_u8()) / 255.0, + "last_input_tick": b.get_u32(), + }) + + var ecount := b.get_u16() + for _i in ecount: + snap["enemies"].append({ + "id": b.get_u32(), + "pos": Vector2(b.get_float(), b.get_float()), + "hp": b.get_u16(), + "radius": float(b.get_u8()) * 0.5, + "visual": b.get_u8(), + }) + + if b.get_u8() == 1: + snap["boss"] = { + "id": b.get_u32(), + "pos": Vector2(b.get_float(), b.get_float()), + "hp": b.get_u32(), + "phase": b.get_u8(), + } + return snap + + +# --- Events ----------------------------------------------------------------- + +## Events the client never sees; the instance layer consumes them server-side. +const SERVER_ONLY := [SimEvent.Type.PORTAL_USED, SimEvent.Type.ESCAPE_COMPLETED] + + +## [param server_tick] rides along so the client can fast-forward a bullet by +## however many ticks the packet spent in flight, instead of popping it in at +## the muzzle a round-trip late. +static func encode_events(server_tick: int, events: Array[Dictionary]) -> PackedByteArray: + var b := StreamPeerBuffer.new() + b.big_endian = false + b.put_u32(server_tick) + var count := 0 + var body := StreamPeerBuffer.new() + body.big_endian = false + for ev in events: + var t := int(ev["t"]) + if SERVER_ONLY.has(t): + continue + body.put_u8(t) + match t: + SimEvent.Type.BULLET_SPAWN: + body.put_u32(ev["uid"]) + body.put_float(ev["pos"].x) + body.put_float(ev["pos"].y) + body.put_float(ev["vel"].x) + body.put_float(ev["vel"].y) + body.put_float(ev["r"]) + body.put_u16(clampi(int(ev["life"]), 0, 65535)) + body.put_u8(int(ev["kind"])) + body.put_u8(int(ev["team"])) + body.put_float(ev["accel"]) + body.put_float(ev["turn"]) + SimEvent.Type.BULLET_DESPAWN: + body.put_u32(ev["uid"]) + SimEvent.Type.PLAYER_HIT: + body.put_u32(ev["peer"]) + body.put_u16(clampi(int(ev["dmg"]), 0, 65535)) + body.put_u16(clampi(int(ev["hp"]), 0, 65535)) + SimEvent.Type.PLAYER_DIED, SimEvent.Type.ESCAPE_STARTED, \ + SimEvent.Type.ESCAPE_CANCELLED: + body.put_u32(ev["peer"]) + SimEvent.Type.PLAYER_RESPAWNED: + body.put_u32(ev["peer"]) + body.put_float(ev["pos"].x) + body.put_float(ev["pos"].y) + SimEvent.Type.ENEMY_HIT: + body.put_u32(ev["id"]) + body.put_u16(clampi(int(ev["dmg"]), 0, 65535)) + body.put_u32(maxi(int(ev["hp"]), 0)) + SimEvent.Type.ENEMY_DIED: + body.put_u32(ev["id"]) + SimEvent.Type.BOSS_PHASE: + body.put_u8(clampi(int(ev["phase"]), 0, 255)) + SimEvent.Type.BOSS_DIED: + pass + count += 1 + b.put_u16(count) + b.put_data(body.data_array) + return b.data_array + + +## Returns { "tick": int, "events": Array[Dictionary] }. +static func decode_events(data: PackedByteArray) -> Dictionary: + var out: Array[Dictionary] = [] + var b := StreamPeerBuffer.new() + b.big_endian = false + b.data_array = data + var server_tick := b.get_u32() + var count := b.get_u16() + for _i in count: + var t := b.get_u8() + var ev := {"t": t} + match t: + SimEvent.Type.BULLET_SPAWN: + ev["uid"] = b.get_u32() + ev["pos"] = Vector2(b.get_float(), b.get_float()) + ev["vel"] = Vector2(b.get_float(), b.get_float()) + ev["r"] = b.get_float() + ev["life"] = b.get_u16() + ev["kind"] = b.get_u8() + ev["team"] = b.get_u8() + ev["accel"] = b.get_float() + ev["turn"] = b.get_float() + SimEvent.Type.BULLET_DESPAWN: + ev["uid"] = b.get_u32() + SimEvent.Type.PLAYER_HIT: + ev["peer"] = b.get_u32() + ev["dmg"] = b.get_u16() + ev["hp"] = b.get_u16() + SimEvent.Type.PLAYER_DIED, SimEvent.Type.ESCAPE_STARTED, \ + SimEvent.Type.ESCAPE_CANCELLED: + ev["peer"] = b.get_u32() + SimEvent.Type.PLAYER_RESPAWNED: + ev["peer"] = b.get_u32() + ev["pos"] = Vector2(b.get_float(), b.get_float()) + SimEvent.Type.ENEMY_HIT: + ev["id"] = b.get_u32() + ev["dmg"] = b.get_u16() + ev["hp"] = b.get_u32() + SimEvent.Type.ENEMY_DIED: + ev["id"] = b.get_u32() + SimEvent.Type.BOSS_PHASE: + ev["phase"] = b.get_u8() + SimEvent.Type.BOSS_DIED: + pass + out.append(ev) + return {"tick": server_tick, "events": out} + + +# --- Input ------------------------------------------------------------------ + +static func encode_inputs(frames: Array[InputFrame]) -> PackedByteArray: + var b := StreamPeerBuffer.new() + b.big_endian = false + b.put_u8(mini(frames.size(), 255)) + for f in frames: + f.write(b) + return b.data_array + + +static func decode_inputs(data: PackedByteArray) -> Array[InputFrame]: + var out: Array[InputFrame] = [] + if data.size() < 1: + return out + var b := StreamPeerBuffer.new() + b.big_endian = false + b.data_array = data + var count := b.get_u8() + # A malformed or hostile packet must not make the server read past the end. + if data.size() < 1 + count * InputFrame.SIZE: + return out + for _i in count: + out.append(InputFrame.read(b)) + return out diff --git a/src/net/net_codec.gd.uid b/src/net/net_codec.gd.uid new file mode 100644 index 0000000..ee2f8ea --- /dev/null +++ b/src/net/net_codec.gd.uid @@ -0,0 +1 @@ +uid://er7seioedeo4 diff --git a/src/net/protocol.gd b/src/net/protocol.gd new file mode 100644 index 0000000..e2be3fe --- /dev/null +++ b/src/net/protocol.gd @@ -0,0 +1,24 @@ +class_name Protocol +extends RefCounted +## Wire constants. Bump [constant VERSION] whenever a codec layout changes; the +## server refuses mismatched clients at handshake rather than desyncing later. + +const VERSION := 1 +const DEFAULT_PORT := 27015 +const MAX_CLIENTS := 32 + +## ENet channels. Separating them stops a burst of reliable bullet events from +## head-of-line blocking the unreliable snapshot stream. +const CH_CONTROL := 1 ## handshake, instance transitions -- reliable +const CH_SNAPSHOT := 2 ## world state -- unreliable, newest wins +const CH_EVENTS := 3 ## bullet spawns/despawns, hits -- reliable ordered +const CH_INPUT := 4 ## client -> server intent -- unreliable ordered +## Must be identical on both ends, and larger than the highest channel above. +const CHANNEL_COUNT := 8 + +enum InstanceKind { LOBBY, DUNGEON } + +## Player flags packed into the snapshot's per-player byte. +const F_ALIVE := 1 +const F_INVULN := 2 +const F_ESCAPING := 4 diff --git a/src/net/protocol.gd.uid b/src/net/protocol.gd.uid new file mode 100644 index 0000000..033b885 --- /dev/null +++ b/src/net/protocol.gd.uid @@ -0,0 +1 @@ +uid://dao4v3ubbg0yt diff --git a/src/net/server_runtime.gd b/src/net/server_runtime.gd new file mode 100644 index 0000000..60e36dd --- /dev/null +++ b/src/net/server_runtime.gd @@ -0,0 +1,193 @@ +class_name ServerRuntime +extends Node +## The dedicated server. Owns every instance, ticks them all at the physics +## rate, and is the only place in the codebase allowed to decide what happened. +## +## Clients send intent and nothing else (see [method SimWorld.queue_input]), so +## there is no client message that can move a player, deal damage, cancel a hit +## or shorten an escape channel. Refusing to accept those messages at all is a +## stronger guarantee than validating them after the fact. + +var instances: Dictionary[int, Instance] = {} +var peer_instance: Dictionary[int, int] = {} +var peer_names: Dictionary[int, String] = {} +var lobby: Instance + +var _next_instance_id: int = SimConfig.LOBBY_INSTANCE_ID +var _snapshot_phase: int = 0 + + +func _ready() -> void: + lobby = Instance.make_lobby(_take_instance_id()) + instances[lobby.id] = lobby + GameLog.info("server", "lobby instance %d up" % lobby.id) + + +func _take_instance_id() -> int: + var id := _next_instance_id + _next_instance_id += 1 + return id + + +func _physics_process(_delta: float) -> void: + _snapshot_phase += 1 + var send_snapshot := _snapshot_phase % SimConfig.SNAPSHOT_INTERVAL == 0 + var closing: Array[int] = [] + + for inst in instances.values(): + inst.step() + _dispatch_events(inst) + if send_snapshot and not inst.peers.is_empty(): + var snap := NetCodec.encode_snapshot(inst.world) + for peer in inst.peers: + Net.send_snapshot(peer, snap) + if inst.kind == Protocol.InstanceKind.DUNGEON \ + and inst.state == Instance.State.CLEARED and inst.stage_delay <= 0: + closing.append(inst.id) + elif inst.kind == Protocol.InstanceKind.DUNGEON and inst.is_empty() and inst.age > 60: + closing.append(inst.id) + + for id in closing: + _close_dungeon(id) + + +## Split the tick's events into the ones clients need and the ones only the +## server acts on (escape completion, portal use). +func _dispatch_events(inst: Instance) -> void: + var events := inst.world.drain_events() + if events.is_empty(): + return + var transfers: Array[Dictionary] = [] + for ev in events: + match int(ev["t"]): + SimEvent.Type.ESCAPE_COMPLETED: + transfers.append({"peer": int(ev["peer"]), "to_lobby": true}) + SimEvent.Type.PORTAL_USED: + transfers.append({"peer": int(ev["peer"]), "to_lobby": false}) + _: + pass + + var payload := NetCodec.encode_events(inst.world.tick, events) + for peer in inst.peers: + Net.send_events(peer, payload) + + for t in transfers: + if t["to_lobby"]: + _send_to_lobby(int(t["peer"])) + else: + _send_to_dungeon(int(t["peer"])) + + +# --- Peer lifecycle --------------------------------------------------------- + +func on_peer_connected(peer_id: int) -> void: + GameLog.info("server", "peer %d connected, awaiting hello" % peer_id) + + +func on_peer_disconnected(peer_id: int) -> void: + var inst := instance_of(peer_id) + if inst != null: + inst.remove_peer(peer_id) + peer_instance.erase(peer_id) + peer_names.erase(peer_id) + GameLog.info("server", "peer %d disconnected" % peer_id) + + +func on_hello(peer_id: int, version: int, display_name: String) -> void: + if peer_names.has(peer_id): + return # a second hello from the same peer is either a bug or an attack + if version != Protocol.VERSION: + GameLog.warn("server", "peer %d protocol %d != %d, rejecting" % [peer_id, version, Protocol.VERSION]) + Net.send_reject(peer_id, "protocol mismatch: server %d, client %d" % [Protocol.VERSION, version]) + Net.kick(peer_id) + return + # Never trust a client-supplied string for anything but display. + var clean := display_name.strip_edges().substr(0, 24) + if clean.is_empty(): + clean = "player%d" % peer_id + peer_names[peer_id] = clean + Net.send_welcome(peer_id) + _place(peer_id, lobby) + GameLog.info("server", "peer %d joined as '%s'" % [peer_id, clean]) + + +func on_input(peer_id: int, data: PackedByteArray) -> void: + var inst := instance_of(peer_id) + if inst == null: + return + inst.world.queue_input(peer_id, NetCodec.decode_inputs(data)) + + +func instance_of(peer_id: int) -> Instance: + var id: int = peer_instance.get(peer_id, 0) + return instances.get(id) + + +# --- Transfers -------------------------------------------------------------- + +func _place(peer_id: int, inst: Instance) -> void: + inst.add_peer(peer_id, peer_names.get(peer_id, "player")) + peer_instance[peer_id] = inst.id + Net.send_enter_instance(peer_id, inst.id, int(inst.kind), inst.world.tick, + String(inst.boss_id), inst.world.spawn_point) + # A player arriving mid-fight has no idea what is already in the air, so + # replay the live bullets as spawn events before the next snapshot lands. + var backlog := _live_bullet_events(inst.world) + if not backlog.is_empty(): + Net.send_events(peer_id, NetCodec.encode_events(inst.world.tick, backlog)) + Net.send_snapshot(peer_id, NetCodec.encode_snapshot(inst.world)) + + +func _transfer(peer_id: int, to: Instance) -> void: + var from := instance_of(peer_id) + if from != null: + from.remove_peer(peer_id) + _place(peer_id, to) + + +func _send_to_lobby(peer_id: int) -> void: + GameLog.info("server", "peer %d escaped to lobby" % peer_id) + _transfer(peer_id, lobby) + + +func _send_to_dungeon(peer_id: int) -> void: + var target: Instance = null + for inst in instances.values(): + if inst.accepts_new_party_member(): + target = inst + break + if target == null: + target = Instance.make_dungeon(_take_instance_id(), randi()) + instances[target.id] = target + GameLog.info("server", "opened dungeon instance %d" % target.id) + _transfer(peer_id, target) + + +func _close_dungeon(id: int) -> void: + var inst: Instance = instances.get(id) + if inst == null: + return + for peer in inst.peers.duplicate(): + _transfer(peer, lobby) + instances.erase(id) + GameLog.info("server", "closed dungeon instance %d" % id) + + +func _live_bullet_events(world: SimWorld) -> Array[Dictionary]: + var out: Array[Dictionary] = [] + for i in world.pool.high_water: + if world.pool.alive[i] == 0: + continue + out.append({ + "t": SimEvent.Type.BULLET_SPAWN, + "uid": world.pool.uid[i], + "pos": world.pool.pos[i], + "vel": world.pool.vel[i], + "r": world.pool.radius[i], + "life": world.pool.life[i], + "kind": world.pool.kind[i], + "team": world.pool.team[i], + "accel": world.pool.accel[i], + "turn": world.pool.turn[i], + }) + return out diff --git a/src/net/server_runtime.gd.uid b/src/net/server_runtime.gd.uid new file mode 100644 index 0000000..a073f14 --- /dev/null +++ b/src/net/server_runtime.gd.uid @@ -0,0 +1 @@ +uid://cfysfvk62ik58 diff --git a/src/sim/bullet_pool.gd b/src/sim/bullet_pool.gd new file mode 100644 index 0000000..852c82c --- /dev/null +++ b/src/sim/bullet_pool.gd @@ -0,0 +1,161 @@ +class_name BulletPool +extends RefCounted +## Bullets are plain data in parallel arrays, never nodes. A bullet-hell spawns +## thousands of them per second; one Node2D + Area2D each would dominate both +## the frame budget and the network. Everything here is integrated by hand so +## the server can run it headless and a unit test can run it with no SceneTree. + +var pos := PackedVector2Array() +var vel := PackedVector2Array() +var radius := PackedFloat32Array() +## Scalar acceleration along the current heading, units/sec^2. +var accel := PackedFloat32Array() +## Heading change per tick, radians. Non-zero makes the bullet curve. +var turn := PackedFloat32Array() +var life := PackedInt32Array() +var damage := PackedInt32Array() +## Network identity. Stable for the bullet's whole life, 0 in a free slot. +var uid := PackedInt32Array() +var team := PackedByteArray() +var kind := PackedByteArray() +var alive := PackedByteArray() + +var live_count := 0 +## Exclusive upper bound over slots that have ever been used, so the hot loops +## do not walk the whole 4096-slot table while the arena is nearly empty. +var high_water := 0 + +var _free := PackedInt32Array() +var _next_uid := 1 + +## Slots spawned since the last [method clear_spawn_log]. The server turns this +## into BULLET_SPAWN events; the client replica ignores it because its bullets +## arrive from the wire already. +var spawn_log := PackedInt32Array() + + +func _init() -> void: + var n := SimConfig.MAX_BULLETS + pos.resize(n) + vel.resize(n) + radius.resize(n) + accel.resize(n) + turn.resize(n) + life.resize(n) + damage.resize(n) + uid.resize(n) + team.resize(n) + kind.resize(n) + alive.resize(n) + + +func clear() -> void: + for i in high_water: + alive[i] = 0 + uid[i] = 0 + _free.clear() + spawn_log.clear() + live_count = 0 + high_water = 0 + + +## Allocate a slot. Returns the slot index, or -1 when the pool is saturated. +## [param forced_uid] is used by the client replica so its bullets carry the +## same identity the server assigned. +func spawn(p: Vector2, v: Vector2, r: float, lifetime: int, dmg: int, + bullet_team: int, bullet_kind: int, bullet_accel: float = 0.0, + bullet_turn: float = 0.0, forced_uid: int = 0) -> int: + var slot := -1 + if not _free.is_empty(): + slot = _free[_free.size() - 1] + _free.remove_at(_free.size() - 1) + elif high_water < SimConfig.MAX_BULLETS: + slot = high_water + high_water += 1 + else: + return -1 + + pos[slot] = p + vel[slot] = v + radius[slot] = r + accel[slot] = bullet_accel + turn[slot] = bullet_turn + life[slot] = lifetime + damage[slot] = dmg + team[slot] = bullet_team + kind[slot] = bullet_kind + alive[slot] = 1 + if forced_uid != 0: + uid[slot] = forced_uid + else: + uid[slot] = _next_uid + _next_uid += 1 + live_count += 1 + spawn_log.append(slot) + return slot + + +func clear_spawn_log() -> void: + spawn_log.clear() + + +func despawn(slot: int) -> void: + if alive[slot] == 0: + return + alive[slot] = 0 + uid[slot] = 0 + live_count -= 1 + _free.append(slot) + + +func find_by_uid(target_uid: int) -> int: + for i in high_water: + if alive[i] == 1 and uid[i] == target_uid: + return i + return -1 + + +## Advance every live bullet one tick. Bullets that expire or leave the arena +## die here on both server and client without any network traffic, because both +## sides run this identical integration. +func step() -> void: + var dt := SimConfig.TICK_DELTA + for i in high_water: + if alive[i] == 0: + continue + var v := vel[i] + if turn[i] != 0.0: + v = v.rotated(turn[i]) + if accel[i] != 0.0: + var speed := v.length() + if speed > 0.0001: + v = v * ((speed + accel[i] * dt) / speed) + vel[i] = v + var p: Vector2 = pos[i] + v * dt + pos[i] = p + life[i] -= 1 + if life[i] <= 0 or Movement.outside_arena(p): + despawn(i) + + +## Fast-forward a single bullet. The client uses this so a bullet that spawned +## on the server N ticks ago appears where it should be now instead of popping +## in at the muzzle a round-trip late. +func advance_slot(slot: int, ticks: int) -> void: + var dt := SimConfig.TICK_DELTA + for _i in ticks: + if alive[slot] == 0: + return + var v := vel[slot] + if turn[slot] != 0.0: + v = v.rotated(turn[slot]) + if accel[slot] != 0.0: + var speed := v.length() + if speed > 0.0001: + v = v * ((speed + accel[slot] * dt) / speed) + vel[slot] = v + pos[slot] = pos[slot] + v * dt + life[slot] -= 1 + if life[slot] <= 0 or Movement.outside_arena(pos[slot]): + despawn(slot) + return diff --git a/src/sim/bullet_pool.gd.uid b/src/sim/bullet_pool.gd.uid new file mode 100644 index 0000000..73188dd --- /dev/null +++ b/src/sim/bullet_pool.gd.uid @@ -0,0 +1 @@ +uid://bd7p18su7nbw2 diff --git a/src/sim/input_frame.gd b/src/sim/input_frame.gd new file mode 100644 index 0000000..58b70ba --- /dev/null +++ b/src/sim/input_frame.gd @@ -0,0 +1,48 @@ +class_name InputFrame +extends RefCounted +## One tick of player intent. This is the only thing a client is allowed to tell +## the server about its own state: no positions, no hits, no damage. + +const BTN_FIRE := 1 +const BTN_ESCAPE := 2 +const BTN_INTERACT := 4 + +## Wire size in bytes: u32 tick, i8 move x/y, u16 aim, u8 buttons. +const SIZE := 9 + +var tick: int = 0 +var move := Vector2.ZERO +var aim: float = 0.0 +var buttons: int = 0 + + +static func make(p_tick: int, p_move: Vector2, p_aim: float, p_buttons: int) -> InputFrame: + var f := InputFrame.new() + f.tick = p_tick + f.move = p_move + f.aim = p_aim + f.buttons = p_buttons + return f + + +func pressed(bit: int) -> bool: + return (buttons & bit) != 0 + + +func write(buf: StreamPeerBuffer) -> void: + buf.put_u32(tick) + # Quantised to a signed byte. The server re-clamps to the unit disc anyway, + # so a hand-crafted packet cannot buy extra speed here. + buf.put_8(clampi(roundi(move.x * 100.0), -100, 100)) + buf.put_8(clampi(roundi(move.y * 100.0), -100, 100)) + buf.put_u16(wrapi(roundi(aim / TAU * 65536.0), 0, 65536)) + buf.put_u8(buttons & 0xFF) + + +static func read(buf: StreamPeerBuffer) -> InputFrame: + var f := InputFrame.new() + f.tick = buf.get_u32() + f.move = Vector2(float(buf.get_8()) / 100.0, float(buf.get_8()) / 100.0) + f.aim = float(buf.get_u16()) / 65536.0 * TAU + f.buttons = buf.get_u8() + return f diff --git a/src/sim/input_frame.gd.uid b/src/sim/input_frame.gd.uid new file mode 100644 index 0000000..852dae2 --- /dev/null +++ b/src/sim/input_frame.gd.uid @@ -0,0 +1 @@ +uid://c3p045w4nf6jo diff --git a/src/sim/patterns/aimed_spread_emitter.gd b/src/sim/patterns/aimed_spread_emitter.gd new file mode 100644 index 0000000..5026028 --- /dev/null +++ b/src/sim/patterns/aimed_spread_emitter.gd @@ -0,0 +1,27 @@ +class_name AimedSpreadEmitter +extends BulletEmitter +## A fan aimed at the nearest player. This is the emitter that punishes standing +## still, so most patterns pair one of these with a geometric emitter. + +@export var count: int = 5 +@export var spread_deg: float = 24.0 +## Random angular jitter. Uses the world RNG, which is seeded per instance so a +## replay of the same seed produces the same fight. +@export var jitter_deg: float = 0.0 +@export var muzzle_offset: float = 24.0 +## Extra speed given to the outermost bullets, which curves the fan forward. +@export var edge_speed_bonus: float = 0.0 + + +func fire(ctx: EmitContext) -> void: + if count <= 0: + return + var aim := ctx.aim_angle() + var spread := deg_to_rad(spread_deg) + for i in count: + var t := 0.0 if count == 1 else (float(i) / float(count - 1)) - 0.5 + var a := aim + spread * t + if jitter_deg > 0.0 and ctx.rng != null: + a += deg_to_rad(ctx.rng.randf_range(-jitter_deg, jitter_deg)) + var scale := 1.0 + edge_speed_bonus * absf(t) * 2.0 + emit_shot(ctx, a, ctx.origin + Vector2.RIGHT.rotated(aim) * muzzle_offset, scale) diff --git a/src/sim/patterns/aimed_spread_emitter.gd.uid b/src/sim/patterns/aimed_spread_emitter.gd.uid new file mode 100644 index 0000000..02e4bcb --- /dev/null +++ b/src/sim/patterns/aimed_spread_emitter.gd.uid @@ -0,0 +1 @@ +uid://b4njg402onjdw diff --git a/src/sim/patterns/arc_sweep_emitter.gd b/src/sim/patterns/arc_sweep_emitter.gd new file mode 100644 index 0000000..a9f7f2d --- /dev/null +++ b/src/sim/patterns/arc_sweep_emitter.gd @@ -0,0 +1,31 @@ +class_name ArcSweepEmitter +extends BulletEmitter +## A laser-like arm that sweeps back and forth. Dense enough to force movement, +## slow enough to read. Pairs well with a ring that fills the space behind it. + +@export var arms: int = 2 +@export var bullets_per_arm: int = 3 +## Distance between bullets along one arm. +@export var arm_spacing: float = 22.0 +## Degrees the arm travels per second at the sweep extremes. +@export var sweep_deg: float = 70.0 +## Seconds for one full back-and-forth. +@export var sweep_period: float = 4.0 +@export var base_angle_deg: float = 0.0 +@export var muzzle_offset: float = 26.0 + + +func fire(ctx: EmitContext) -> void: + if arms <= 0 or bullets_per_arm <= 0: + return + var t := float(ctx.local_tick) * SimConfig.TICK_DELTA + var phase := sin(TAU * t / maxf(sweep_period, 0.001)) + var base := deg_to_rad(base_angle_deg + sweep_deg * phase) + for arm in arms: + var a := base + TAU * float(arm) / float(arms) + var heading := Vector2.RIGHT.rotated(a) + for j in bullets_per_arm: + var from := ctx.origin + heading * (muzzle_offset + arm_spacing * float(j)) + # Outer bullets start further along, so give them matching speed to + # keep the arm straight instead of letting it bow. + emit_shot(ctx, a, from, 1.0 + 0.08 * float(j)) diff --git a/src/sim/patterns/arc_sweep_emitter.gd.uid b/src/sim/patterns/arc_sweep_emitter.gd.uid new file mode 100644 index 0000000..86d748f --- /dev/null +++ b/src/sim/patterns/arc_sweep_emitter.gd.uid @@ -0,0 +1 @@ +uid://bxgv7yc1b7rqx diff --git a/src/sim/patterns/bullet_emitter.gd b/src/sim/patterns/bullet_emitter.gd new file mode 100644 index 0000000..588c477 --- /dev/null +++ b/src/sim/patterns/bullet_emitter.gd @@ -0,0 +1,60 @@ +@abstract +class_name BulletEmitter +extends Resource +## One firing behaviour on a timeline. A boss phase or an enemy is just a list +## of these, which is the whole "format that adapts to other bosses": authoring +## a new boss means writing a new list, never new code. + +@export_group("Timeline") +## First tick of the owner's phase at which this emitter is armed. +@export var start_tick: int = 0 +## Last armed tick, or -1 to stay armed for the whole phase. +@export var end_tick: int = -1 +## Ticks between shots. +@export var interval: int = 30 + +@export_group("Bullet") +@export var speed: float = 150.0 +## Units/sec^2 along the heading. Negative values make bullets stall and drift. +@export var accel: float = 0.0 +## Degrees of heading change per tick. Non-zero produces curving bullets. +@export var turn_deg: float = 0.0 +@export var radius: float = 7.0 +@export var lifetime: int = 300 +@export var damage: int = 12 +@export var kind: int = SimConfig.KIND_ORB + + +func is_armed(local_tick: int) -> bool: + if local_tick < start_tick: + return false + if end_tick >= 0 and local_tick > end_tick: + return false + return true + + +func should_fire(local_tick: int) -> bool: + if not is_armed(local_tick): + return false + if interval <= 0: + return false + return (local_tick - start_tick) % interval == 0 + + +## How many shots this emitter has fired by [param local_tick], used to drive +## per-shot spin without the emitter holding mutable state (emitters are shared +## resources -- two bosses of the same kind must not stomp each other). +func shot_index_at(local_tick: int) -> int: + if interval <= 0: + return 0 + return int(floor(float(local_tick - start_tick) / float(interval))) + + +@abstract func fire(ctx: EmitContext) -> void + + +## Shared spawn path so every emitter produces bullets with identical physics. +func emit_shot(ctx: EmitContext, angle: float, from: Vector2, speed_scale: float = 1.0) -> void: + var v := Vector2.RIGHT.rotated(angle) * speed * speed_scale + ctx.pool.spawn(from, v, radius, lifetime, damage, SimConfig.TEAM_ENEMY, kind, + accel, deg_to_rad(turn_deg)) diff --git a/src/sim/patterns/bullet_emitter.gd.uid b/src/sim/patterns/bullet_emitter.gd.uid new file mode 100644 index 0000000..a54f98d --- /dev/null +++ b/src/sim/patterns/bullet_emitter.gd.uid @@ -0,0 +1 @@ +uid://c27tahmytk5vj diff --git a/src/sim/patterns/emit_context.gd b/src/sim/patterns/emit_context.gd new file mode 100644 index 0000000..ccfd0e5 --- /dev/null +++ b/src/sim/patterns/emit_context.gd @@ -0,0 +1,23 @@ +class_name EmitContext +extends RefCounted +## Everything an emitter is allowed to see. Keeping this narrow is what lets a +## unit test fire a boss pattern into a bare [BulletPool] with no world at all. + +var pool: BulletPool +## Where the shots come from (the emitting actor's position). +var origin := Vector2.ZERO +## Nearest live player, used by aimed emitters. Falls back to [member origin] +## plus a down vector when nobody is alive. +var target := Vector2.ZERO +var has_target := false +## Ticks since the emitting actor entered its current phase. +var local_tick := 0 +## How many times this emitter has fired in this phase. Drives spin/step. +var shot_index := 0 +var rng: RandomNumberGenerator + + +func aim_angle() -> float: + if not has_target: + return PI * 0.5 + return (target - origin).angle() diff --git a/src/sim/patterns/emit_context.gd.uid b/src/sim/patterns/emit_context.gd.uid new file mode 100644 index 0000000..c79c1ad --- /dev/null +++ b/src/sim/patterns/emit_context.gd.uid @@ -0,0 +1 @@ +uid://cav5jlkxsy6qm diff --git a/src/sim/patterns/ring_emitter.gd b/src/sim/patterns/ring_emitter.gd new file mode 100644 index 0000000..d76dc6d --- /dev/null +++ b/src/sim/patterns/ring_emitter.gd @@ -0,0 +1,29 @@ +class_name RingEmitter +extends BulletEmitter +## Evenly spaced bullets around a full or partial circle. Give it a non-zero +## [member spin_per_shot_deg] and consecutive rings become a spiral. + +@export var count: int = 12 +## Total angle the bullets are spread over. 360 is a closed ring. +@export var arc_deg: float = 360.0 +@export var base_angle_deg: float = 0.0 +## Added to the base angle on every shot. +@export var spin_per_shot_deg: float = 7.0 +## Spawn distance from the emitter, so big bosses do not fire from their centre. +@export var muzzle_offset: float = 24.0 +## Aim the ring's base angle at the nearest player instead of using world space. +@export var aim_at_target: bool = false + + +func fire(ctx: EmitContext) -> void: + if count <= 0: + return + var base := deg_to_rad(base_angle_deg + spin_per_shot_deg * float(ctx.shot_index)) + if aim_at_target: + base += ctx.aim_angle() + var arc := deg_to_rad(arc_deg) + # A closed ring must not place two bullets on top of each other at the seam. + var divisor := count if is_equal_approx(arc_deg, 360.0) else maxi(count - 1, 1) + for i in count: + var a := base + arc * (float(i) / float(divisor)) + emit_shot(ctx, a, ctx.origin + Vector2.RIGHT.rotated(a) * muzzle_offset) diff --git a/src/sim/patterns/ring_emitter.gd.uid b/src/sim/patterns/ring_emitter.gd.uid new file mode 100644 index 0000000..10f4275 --- /dev/null +++ b/src/sim/patterns/ring_emitter.gd.uid @@ -0,0 +1 @@ +uid://c5664w2onh7wx diff --git a/src/sim/patterns/wall_gap_emitter.gd b/src/sim/patterns/wall_gap_emitter.gd new file mode 100644 index 0000000..6db5825 --- /dev/null +++ b/src/sim/patterns/wall_gap_emitter.gd @@ -0,0 +1,47 @@ +class_name WallGapEmitter +extends BulletEmitter +## A curtain of bullets sweeping across the arena with a survivable gap. The gap +## moves by [member gap_step] every shot so players cannot camp one lane. + +## 0 = travels down, 1 = up, 2 = right, 3 = left. +@export_enum("Down", "Up", "Right", "Left") var direction: int = 0 +@export var count: int = 18 +## Slots left empty. Width 2-3 is tight but fair at default bullet radius. +@export var gap_width: int = 3 +@export var gap_index: int = 6 +## How far the gap slides each shot. Coprime-ish values feel least predictable. +@export var gap_step: int = 5 +## Randomise the gap instead of stepping it. +@export var randomize_gap: bool = false + + +func _axis() -> Dictionary: + match direction: + 1: return {"dir": Vector2.UP, "along": Vector2.RIGHT, "extent": SimConfig.ARENA_HALF.x, "edge": SimConfig.ARENA_HALF.y} + 2: return {"dir": Vector2.RIGHT, "along": Vector2.DOWN, "extent": SimConfig.ARENA_HALF.y, "edge": SimConfig.ARENA_HALF.x} + 3: return {"dir": Vector2.LEFT, "along": Vector2.DOWN, "extent": SimConfig.ARENA_HALF.y, "edge": SimConfig.ARENA_HALF.x} + _: return {"dir": Vector2.DOWN, "along": Vector2.RIGHT, "extent": SimConfig.ARENA_HALF.x, "edge": SimConfig.ARENA_HALF.y} + + +func fire(ctx: EmitContext) -> void: + if count <= 0: + return + var ax := _axis() + var dir: Vector2 = ax["dir"] + var along: Vector2 = ax["along"] + var extent: float = ax["extent"] + var edge: float = ax["edge"] + + var gap := gap_index + if randomize_gap and ctx.rng != null: + gap = ctx.rng.randi_range(0, maxi(count - gap_width, 0)) + else: + gap = posmod(gap_index + gap_step * ctx.shot_index, maxi(count - gap_width + 1, 1)) + + var start := -dir * (edge + 8.0) + var angle := dir.angle() + for i in count: + if i >= gap and i < gap + gap_width: + continue + var t := (float(i) / float(maxi(count - 1, 1))) * 2.0 - 1.0 + emit_shot(ctx, angle, start + along * (t * extent)) diff --git a/src/sim/patterns/wall_gap_emitter.gd.uid b/src/sim/patterns/wall_gap_emitter.gd.uid new file mode 100644 index 0000000..2d2b6ea --- /dev/null +++ b/src/sim/patterns/wall_gap_emitter.gd.uid @@ -0,0 +1 @@ +uid://igr031g85v5q diff --git a/src/sim/sim_boss.gd b/src/sim/sim_boss.gd new file mode 100644 index 0000000..4a7b99d --- /dev/null +++ b/src/sim/sim_boss.gd @@ -0,0 +1,25 @@ +class_name SimBoss +extends RefCounted +## A boss instance. All fight-specific behaviour lives in its [BossDef]; this +## class only advances the phase timeline. + +var id: int = 0 +var def: BossDef +var pos := Vector2.ZERO +var hp: int = 1 +var alive: bool = true +var phase_index: int = 0 +## Ticks since entering the current phase. +var phase_tick: int = 0 + + +func hp_fraction() -> float: + if def == null or def.max_hp <= 0: + return 0.0 + return float(hp) / float(def.max_hp) + + +func current_phase() -> BossPhase: + if def == null or def.phases.is_empty(): + return null + return def.phases[clampi(phase_index, 0, def.phases.size() - 1)] diff --git a/src/sim/sim_boss.gd.uid b/src/sim/sim_boss.gd.uid new file mode 100644 index 0000000..0c0bd8e --- /dev/null +++ b/src/sim/sim_boss.gd.uid @@ -0,0 +1 @@ +uid://beq5lnuth5prk diff --git a/src/sim/sim_enemy.gd b/src/sim/sim_enemy.gd new file mode 100644 index 0000000..35eb139 --- /dev/null +++ b/src/sim/sim_enemy.gd @@ -0,0 +1,23 @@ +class_name SimEnemy +extends RefCounted +## A trash enemy instance. Movement is deliberately legible -- every behaviour +## here is a closed-form function of tick and spawn point, so a player can learn +## it, and so a test can assert on it. + +var id: int = 0 +var def: EnemyDef +var pos := Vector2.ZERO +var home := Vector2.ZERO +var heading := Vector2.RIGHT +var hp: int = 1 +var alive: bool = true +var local_tick: int = 0 +## Staggers identical enemies so a pack does not fire in lockstep. +var phase_offset: int = 0 +var target_dir := Vector2.ZERO + + +func hp_fraction() -> float: + if def == null or def.max_hp <= 0: + return 0.0 + return float(hp) / float(def.max_hp) diff --git a/src/sim/sim_enemy.gd.uid b/src/sim/sim_enemy.gd.uid new file mode 100644 index 0000000..d92b382 --- /dev/null +++ b/src/sim/sim_enemy.gd.uid @@ -0,0 +1 @@ +uid://cbax6i5kwsr5w diff --git a/src/sim/sim_events.gd b/src/sim/sim_events.gd new file mode 100644 index 0000000..79cef29 --- /dev/null +++ b/src/sim/sim_events.gd @@ -0,0 +1,22 @@ +class_name SimEvent +extends RefCounted +## Discrete things the authoritative simulation decided. The server drains this +## list every tick and ships it to clients on a reliable channel; clients apply +## them to their replica. Continuous state (positions, hp) goes in snapshots +## instead -- events are only for things a client can never re-derive. + +enum Type { + BULLET_SPAWN, ## uid, pos, vel, radius, life, kind, team, accel, turn + BULLET_DESPAWN, ## uid -- early death only; expiry is derived on both sides + PLAYER_HIT, ## peer, damage, hp + PLAYER_DIED, ## peer + PLAYER_RESPAWNED, ## peer, pos + ENEMY_HIT, ## id, damage, hp + ENEMY_DIED, ## id + BOSS_PHASE, ## phase index + BOSS_DIED, + ESCAPE_STARTED, ## peer + ESCAPE_CANCELLED, ## peer + ESCAPE_COMPLETED, ## peer -- the instance layer acts on this + PORTAL_USED, ## peer -- the instance layer acts on this +} diff --git a/src/sim/sim_events.gd.uid b/src/sim/sim_events.gd.uid new file mode 100644 index 0000000..e9f1d5a --- /dev/null +++ b/src/sim/sim_events.gd.uid @@ -0,0 +1 @@ +uid://bqs0ihl1uh8b0 diff --git a/src/sim/sim_player.gd b/src/sim/sim_player.gd new file mode 100644 index 0000000..7535ef6 --- /dev/null +++ b/src/sim/sim_player.gd @@ -0,0 +1,41 @@ +class_name SimPlayer +extends RefCounted +## Authoritative player state. Lives only inside a [SimWorld]; the client holds +## a mirror of it for rendering and prediction. + +var peer_id: int = 0 +var display_name: String = "player" +var pos := Vector2.ZERO +var aim: float = 0.0 +var hp: int = SimConfig.PLAYER_MAX_HP +var alive: bool = true +var iframes: int = 0 +var fire_cooldown: int = 0 +var respawn_timer: int = 0 + +## Ticks the emergency escape has been held. 0 means not channelling. +var escape_ticks: int = 0 +## Newest input tick the server has consumed. Echoed back in snapshots so the +## client knows how far to rewind when reconciling. +var last_input_tick: int = 0 + +var input_queue: Array[InputFrame] = [] +## Repeated when the queue runs dry, so a dropped packet coasts instead of +## stuttering. Capped by [member starved_ticks]. +var held_input: InputFrame = InputFrame.new() +var starved_ticks: int = 0 + + +func escape_progress() -> float: + return clampf(float(escape_ticks) / float(SimConfig.ESCAPE_CHANNEL_TICKS), 0.0, 1.0) + + +func reset_for_instance(spawn: Vector2) -> void: + pos = spawn + hp = SimConfig.PLAYER_MAX_HP + alive = true + iframes = SimConfig.PLAYER_IFRAMES + fire_cooldown = 0 + respawn_timer = 0 + escape_ticks = 0 + input_queue.clear() diff --git a/src/sim/sim_player.gd.uid b/src/sim/sim_player.gd.uid new file mode 100644 index 0000000..3b357f4 --- /dev/null +++ b/src/sim/sim_player.gd.uid @@ -0,0 +1 @@ +uid://qjogi5l7e1ab diff --git a/src/sim/sim_world.gd b/src/sim/sim_world.gd new file mode 100644 index 0000000..e833afb --- /dev/null +++ b/src/sim/sim_world.gd @@ -0,0 +1,430 @@ +class_name SimWorld +extends RefCounted +## The whole game simulation for one instance (a lobby or one dungeon run). +## +## It is a plain RefCounted with no nodes, no physics server and no rendering, +## which buys three things: the dedicated server runs it headless at negligible +## cost, a unit test can drive a thousand ticks in milliseconds, and the client +## can run the exact same class in replica mode for prediction. +## +## Authority: [member authoritative] is true on the server only. In replica mode +## the world never runs AI, never fires emitters and never resolves a hit -- it +## only integrates bullets it was told about and holds actor state for drawing. +## That is the whole anti-cheat story: a client physically has no code path that +## can decide it dealt or avoided damage. + +var tick: int = 0 +var authoritative: bool = true +var pool := BulletPool.new() +var rng := RandomNumberGenerator.new() + +var players: Dictionary[int, SimPlayer] = {} +var enemies: Dictionary[int, SimEnemy] = {} +var boss: SimBoss = null + +## Drained by the owner every tick. See [SimEvent]. +var events: Array[Dictionary] = [] + +## Set on a lobby world so the interact button can open a dungeon. +var portal_enabled: bool = false +var spawn_point := Vector2(0.0, 240.0) + +var _next_actor_id: int = 1 +var _ctx := EmitContext.new() + + +func _init(seed_value: int = 0) -> void: + rng.seed = seed_value + _ctx.rng = rng + + +func next_actor_id() -> int: + var id := _next_actor_id + _next_actor_id += 1 + return id + + +# --- Population ------------------------------------------------------------- + +func add_player(peer_id: int, display_name: String) -> SimPlayer: + var p := SimPlayer.new() + p.peer_id = peer_id + p.display_name = display_name + p.reset_for_instance(spawn_point) + players[peer_id] = p + return p + + +func remove_player(peer_id: int) -> void: + players.erase(peer_id) + + +func spawn_enemy(def: EnemyDef, at: Vector2, phase_offset: int = 0) -> SimEnemy: + var e := SimEnemy.new() + e.id = next_actor_id() + e.def = def + e.pos = at + e.home = at + e.hp = def.max_hp + e.phase_offset = phase_offset + e.heading = Vector2.RIGHT.rotated(rng.randf() * TAU) + enemies[e.id] = e + return e + + +func spawn_boss(def: BossDef) -> SimBoss: + var b := SimBoss.new() + b.id = next_actor_id() + b.def = def + b.pos = def.spawn_pos + b.hp = def.max_hp + boss = b + return b + + +func alive_player_count() -> int: + var n := 0 + for p in players.values(): + if p.alive: + n += 1 + return n + + +## Nearest living player to [param from]. Aimed emitters and chasing enemies use +## this; it is the only targeting primitive in the game. +func nearest_player(from: Vector2) -> SimPlayer: + var best: SimPlayer = null + var best_d := INF + for p in players.values(): + if not p.alive: + continue + var d := from.distance_squared_to(p.pos) + if d < best_d: + best_d = d + best = p + return best + + +# --- Input ------------------------------------------------------------------ + +## Accept a batch of client inputs. Everything questionable is dropped here +## rather than deeper in, so there is a single place to audit. +func queue_input(peer_id: int, frames: Array[InputFrame]) -> void: + var p: SimPlayer = players.get(peer_id) + if p == null: + return + for f in frames: + if f.tick <= p.last_input_tick: + continue # replay or out-of-order duplicate + if f.tick < tick - SimConfig.INPUT_MAX_AGE: + continue # too old to matter + if f.tick > tick + SimConfig.INPUT_MAX_LEAD: + continue # client claiming to be far in the future + if p.input_queue.size() >= SimConfig.INPUT_MAX_AGE: + p.input_queue.pop_front() # flood guard + p.input_queue.append(f) + p.input_queue.sort_custom(func(a: InputFrame, b: InputFrame) -> bool: return a.tick < b.tick) + + +# --- Tick ------------------------------------------------------------------- + +func step() -> void: + tick += 1 + if authoritative: + pool.clear_spawn_log() + _step_players() + _step_enemies() + _step_boss() + pool.step() + _resolve_bullet_hits() + _resolve_contact_damage() + _emit_spawn_events() + else: + # Replica: bullets only. Actor state arrives in snapshots. + pool.step() + + +func _step_players() -> void: + for p in players.values(): + if p.iframes > 0: + p.iframes -= 1 + if p.fire_cooldown > 0: + p.fire_cooldown -= 1 + + if not p.alive: + p.respawn_timer -= 1 + if p.respawn_timer <= 0: + p.alive = true + p.hp = SimConfig.PLAYER_MAX_HP + p.pos = spawn_point + p.iframes = SimConfig.PLAYER_IFRAMES + events.append({"t": SimEvent.Type.PLAYER_RESPAWNED, "peer": p.peer_id, "pos": p.pos}) + continue + + var frame := _take_input(p) + p.aim = frame.aim + p.pos = Movement.step_player(p.pos, frame.move, SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + + if frame.pressed(InputFrame.BTN_FIRE) and p.fire_cooldown == 0: + _fire_player_shot(p) + + _step_escape(p, frame) + + if portal_enabled and frame.pressed(InputFrame.BTN_INTERACT): + if p.pos.distance_to(SimConfig.PORTAL_POS) <= SimConfig.PORTAL_RADIUS: + events.append({"t": SimEvent.Type.PORTAL_USED, "peer": p.peer_id}) + + +## Pull the next input for this player, or coast on the last one. Coasting is +## capped so a client that stops sending cannot keep moving forever. +func _take_input(p: SimPlayer) -> InputFrame: + if not p.input_queue.is_empty(): + var f: InputFrame = p.input_queue.pop_front() + p.last_input_tick = f.tick + p.held_input = f + p.starved_ticks = 0 + return f + p.starved_ticks += 1 + if p.starved_ticks > SimConfig.INPUT_MAX_AGE: + var idle := InputFrame.new() + idle.aim = p.held_input.aim + p.held_input = idle + return p.held_input + + +func _fire_player_shot(p: SimPlayer) -> void: + p.fire_cooldown = SimConfig.PLAYER_FIRE_COOLDOWN + var dir := Vector2.RIGHT.rotated(p.aim) + pool.spawn( + p.pos + dir * (SimConfig.PLAYER_RADIUS + 6.0), + dir * SimConfig.PLAYER_BULLET_SPEED, + SimConfig.PLAYER_BULLET_RADIUS, + SimConfig.PLAYER_BULLET_LIFETIME, + SimConfig.PLAYER_BULLET_DAMAGE, + SimConfig.TEAM_PLAYER, + SimConfig.KIND_PLAYER_SHOT) + + +func _step_escape(p: SimPlayer, frame: InputFrame) -> void: + if frame.pressed(InputFrame.BTN_ESCAPE): + if p.escape_ticks == 0: + events.append({"t": SimEvent.Type.ESCAPE_STARTED, "peer": p.peer_id}) + p.escape_ticks += 1 + if p.escape_ticks >= SimConfig.ESCAPE_CHANNEL_TICKS: + p.escape_ticks = 0 + events.append({"t": SimEvent.Type.ESCAPE_COMPLETED, "peer": p.peer_id}) + elif p.escape_ticks > 0: + p.escape_ticks = 0 + events.append({"t": SimEvent.Type.ESCAPE_CANCELLED, "peer": p.peer_id}) + + +func _step_enemies() -> void: + for e in enemies.values(): + if not e.alive: + continue + _move_enemy(e) + _run_emitters(e.def.emitters, e.pos, + posmod(e.local_tick + e.phase_offset, maxi(e.def.pattern_loop_ticks, 1))) + e.local_tick += 1 + + +func _move_enemy(e: SimEnemy) -> void: + var dt := SimConfig.TICK_DELTA + var speed: float = e.def.speed + match e.def.move: + EnemyDef.Move.STATIC: + pass + EnemyDef.Move.DRIFT: + var next := e.pos + e.heading * speed * dt + # Bounce off the arena so a drifter never leaves the fight. + if absf(next.x) > SimConfig.ARENA_HALF.x - e.def.radius: + e.heading.x = -e.heading.x + next.x = clampf(next.x, -SimConfig.ARENA_HALF.x + e.def.radius, SimConfig.ARENA_HALF.x - e.def.radius) + if absf(next.y) > SimConfig.ARENA_HALF.y - e.def.radius: + e.heading.y = -e.heading.y + next.y = clampf(next.y, -SimConfig.ARENA_HALF.y + e.def.radius, SimConfig.ARENA_HALF.y - e.def.radius) + e.pos = next + EnemyDef.Move.ORBIT: + var a := float(e.local_tick + e.phase_offset) * dt * (speed / maxf(e.def.move_param, 1.0)) + e.pos = e.home + Vector2.RIGHT.rotated(a) * e.def.move_param + EnemyDef.Move.APPROACH: + if e.local_tick % maxi(e.def.retarget_interval, 1) == 0: + var t := nearest_player(e.pos) + e.target_dir = Vector2.ZERO if t == null else (t.pos - e.pos).normalized() + e.pos += e.target_dir * speed * dt + EnemyDef.Move.STRAFE: + if e.local_tick % maxi(e.def.retarget_interval, 1) == 0: + var t := nearest_player(e.pos) + if t == null: + e.target_dir = Vector2.ZERO + else: + var to_player := t.pos - e.pos + var d := to_player.length() + var radial := 0.0 + if d > e.def.move_param + 20.0: + radial = 1.0 + elif d < e.def.move_param - 20.0: + radial = -1.0 + var toward := Vector2.ZERO if d < 0.001 else to_player / d + e.target_dir = (toward * radial + toward.orthogonal() * 0.7).normalized() + e.pos += e.target_dir * speed * dt + e.pos.x = clampf(e.pos.x, -SimConfig.ARENA_HALF.x, SimConfig.ARENA_HALF.x) + e.pos.y = clampf(e.pos.y, -SimConfig.ARENA_HALF.y, SimConfig.ARENA_HALF.y) + + +func _step_boss() -> void: + if boss == null or not boss.alive or boss.def == null: + return + var wanted := boss.def.phase_index_for(boss.hp_fraction()) + if wanted != boss.phase_index: + boss.phase_index = wanted + boss.phase_tick = 0 + events.append({"t": SimEvent.Type.BOSS_PHASE, "phase": wanted}) + var phase := boss.current_phase() + if phase == null: + return + if boss.phase_tick >= phase.telegraph_ticks: + var local := posmod(boss.phase_tick - phase.telegraph_ticks, maxi(phase.loop_ticks, 1)) + _run_emitters(phase.emitters, boss.pos, local) + boss.phase_tick += 1 + + +## Shared emitter driver for enemies and bosses -- the reason a boss pattern can +## be dropped onto a trash mob and vice versa. +func _run_emitters(emitters: Array[BulletEmitter], origin: Vector2, local_tick: int) -> void: + if emitters.is_empty(): + return + var target := nearest_player(origin) + _ctx.pool = pool + _ctx.origin = origin + _ctx.local_tick = local_tick + _ctx.has_target = target != null + _ctx.target = origin + Vector2.DOWN * 200.0 if target == null else target.pos + for e in emitters: + if e == null or not e.should_fire(local_tick): + continue + _ctx.shot_index = e.shot_index_at(local_tick) + e.fire(_ctx) + + +# --- Hit resolution (server only) ------------------------------------------- + +func _resolve_bullet_hits() -> void: + for i in pool.high_water: + if pool.alive[i] == 0: + continue + var bp: Vector2 = pool.pos[i] + var br: float = pool.radius[i] + if pool.team[i] == SimConfig.TEAM_ENEMY: + for p in players.values(): + if not p.alive or p.iframes > 0: + continue + if Movement.circles_overlap(bp, br, p.pos, SimConfig.PLAYER_RADIUS): + _damage_player(p, pool.damage[i]) + _kill_bullet(i) + break + else: + var consumed := false + if boss != null and boss.alive \ + and Movement.circles_overlap(bp, br, boss.pos, boss.def.radius): + _damage_boss(pool.damage[i]) + consumed = true + if not consumed: + for e in enemies.values(): + if not e.alive: + continue + if Movement.circles_overlap(bp, br, e.pos, e.def.radius): + _damage_enemy(e, pool.damage[i]) + consumed = true + break + if consumed: + _kill_bullet(i) + + +func _resolve_contact_damage() -> void: + for e in enemies.values(): + if not e.alive or e.def.contact_damage <= 0: + continue + for p in players.values(): + if not p.alive or p.iframes > 0: + continue + if Movement.circles_overlap(e.pos, e.def.radius, p.pos, SimConfig.PLAYER_RADIUS): + _damage_player(p, e.def.contact_damage) + + +## Bullets removed early must be announced -- clients cannot derive a hit. +func _kill_bullet(slot: int) -> void: + events.append({"t": SimEvent.Type.BULLET_DESPAWN, "uid": pool.uid[slot]}) + pool.despawn(slot) + + +func _damage_player(p: SimPlayer, amount: int) -> void: + p.hp = maxi(p.hp - amount, 0) + p.iframes = SimConfig.PLAYER_IFRAMES + if SimConfig.ESCAPE_BREAK_ON_DAMAGE and p.escape_ticks > 0: + p.escape_ticks = 0 + events.append({"t": SimEvent.Type.ESCAPE_CANCELLED, "peer": p.peer_id}) + events.append({"t": SimEvent.Type.PLAYER_HIT, "peer": p.peer_id, "dmg": amount, "hp": p.hp}) + if p.hp <= 0: + p.alive = false + p.respawn_timer = SimConfig.PLAYER_RESPAWN_DELAY + events.append({"t": SimEvent.Type.PLAYER_DIED, "peer": p.peer_id}) + + +func _damage_enemy(e: SimEnemy, amount: int) -> void: + e.hp = maxi(e.hp - amount, 0) + events.append({"t": SimEvent.Type.ENEMY_HIT, "id": e.id, "dmg": amount, "hp": e.hp}) + if e.hp <= 0: + e.alive = false + events.append({"t": SimEvent.Type.ENEMY_DIED, "id": e.id}) + + +func _damage_boss(amount: int) -> void: + var phase := boss.current_phase() + var mult := 1.0 if phase == null else phase.damage_taken_mult + var applied := maxi(1, roundi(float(amount) * mult)) + boss.hp = maxi(boss.hp - applied, 0) + events.append({"t": SimEvent.Type.ENEMY_HIT, "id": boss.id, "dmg": applied, "hp": boss.hp}) + if boss.hp <= 0: + boss.alive = false + events.append({"t": SimEvent.Type.BOSS_DIED}) + + +func _emit_spawn_events() -> void: + for slot in pool.spawn_log: + if pool.alive[slot] == 0: + continue # spawned and killed inside the same tick + events.append({ + "t": SimEvent.Type.BULLET_SPAWN, + "uid": pool.uid[slot], + "pos": pool.pos[slot], + "vel": pool.vel[slot], + "r": pool.radius[slot], + "life": pool.life[slot], + "kind": pool.kind[slot], + "team": pool.team[slot], + "accel": pool.accel[slot], + "turn": pool.turn[slot], + }) + + +func drain_events() -> Array[Dictionary]: + var out := events + events = [] + return out + + +# --- Replica side ----------------------------------------------------------- + +## Apply a server event to a non-authoritative world. +func apply_event(ev: Dictionary) -> void: + match int(ev["t"]): + SimEvent.Type.BULLET_SPAWN: + pool.spawn(ev["pos"], ev["vel"], ev["r"], ev["life"], 0, + ev["team"], ev["kind"], ev["accel"], ev["turn"], ev["uid"]) + SimEvent.Type.BULLET_DESPAWN: + var slot := pool.find_by_uid(int(ev["uid"])) + if slot >= 0: + pool.despawn(slot) + _: + pass diff --git a/src/sim/sim_world.gd.uid b/src/sim/sim_world.gd.uid new file mode 100644 index 0000000..6f8ac8b --- /dev/null +++ b/src/sim/sim_world.gd.uid @@ -0,0 +1 @@ +uid://o5amgfbx7fkq diff --git a/src/ui/hud.gd b/src/ui/hud.gd new file mode 100644 index 0000000..60a6b79 --- /dev/null +++ b/src/ui/hud.gd @@ -0,0 +1,114 @@ +extends CanvasLayer +## Heads-up display. Built in code rather than as a scene because every element +## is data-driven -- there is no layout here a designer would want to drag. + +const MARGIN := 24.0 +const BAR_W := 260.0 +const BAR_H := 16.0 + +var _canvas: Control +var _status: Label +var _hint: Label +var _hit_flash: float = 0.0 +var client: ClientRuntime = null + + +func _ready() -> void: + layer = 10 + _canvas = Control.new() + _canvas.set_anchors_preset(Control.PRESET_FULL_RECT) + _canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE + _canvas.draw.connect(_draw_hud) + add_child(_canvas) + + _status = _make_label(Vector2(MARGIN, MARGIN)) + _hint = _make_label(Vector2(MARGIN, 0.0)) + _hint.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) + _hint.position = Vector2(MARGIN, -56.0) + + +func _make_label(pos: Vector2) -> Label: + var l := Label.new() + l.position = pos + l.add_theme_font_size_override("font_size", 14) + l.add_theme_color_override("font_color", Color(0.8, 0.85, 0.95)) + _canvas.add_child(l) + return l + + +func _process(delta: float) -> void: + client = Net.client + _hit_flash = maxf(_hit_flash - delta * 2.5, 0.0) + _status.text = _status_text() + _hint.text = _hint_text() + _canvas.queue_redraw() + + +func _status_text() -> String: + if client == null: + return "connecting..." + var where := "LOBBY" if client.instance_kind == Protocol.InstanceKind.LOBBY else "DUNGEON" + return "%s instance %d hp %d/%d %d fps" % [ + where, client.instance_id, client.my_hp, SimConfig.PLAYER_MAX_HP, + Engine.get_frames_per_second()] + + +func _hint_text() -> String: + if client == null: + return "" + if client.instance_kind == Protocol.InstanceKind.LOBBY: + return "WASD move mouse aim LMB fire E on the ring to enter a dungeon" + return "WASD move mouse aim LMB fire hold F to escape to the lobby" + + +func _draw_hud() -> void: + if client == null: + return + var origin := Vector2(MARGIN, MARGIN + 28.0) + _bar(origin, float(client.my_hp) / float(SimConfig.PLAYER_MAX_HP), + Color(0.35, 0.9, 0.6), Color(0.1, 0.15, 0.18)) + + if client.my_escaping: + _bar(origin + Vector2(0.0, BAR_H + 8.0), client.my_escape, + Color(0.5, 0.85, 1.0), Color(0.1, 0.15, 0.2)) + _canvas.draw_string(ThemeDB.fallback_font, + origin + Vector2(BAR_W + 12.0, BAR_H + 8.0 + BAR_H), + "ESCAPING", HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(0.5, 0.85, 1.0)) + + _draw_boss_bar() + + if not client.my_alive: + var size := _canvas.size + _canvas.draw_string(ThemeDB.fallback_font, size * 0.5 - Vector2(70.0, 0.0), + "DOWN -- respawning", HORIZONTAL_ALIGNMENT_LEFT, -1, 22, Color(1.0, 0.4, 0.4)) + + if _hit_flash > 0.0: + _canvas.draw_rect(Rect2(Vector2.ZERO, _canvas.size), + Color(1.0, 0.2, 0.25, 0.18 * _hit_flash)) + + +func _draw_boss_bar() -> void: + var b := client.boss_state() + if b.is_empty() or client.boss_def == null: + return + var w := 560.0 + var pos := Vector2((_canvas.size.x - w) * 0.5, MARGIN) + var frac := clampf(float(b["hp"]) / float(client.boss_def.max_hp), 0.0, 1.0) + _canvas.draw_rect(Rect2(pos, Vector2(w, BAR_H)), Color(0.12, 0.08, 0.1)) + _canvas.draw_rect(Rect2(pos, Vector2(w * frac, BAR_H)), Color(0.95, 0.35, 0.45)) + var phase_index := int(b["phase"]) + var phase_name := "" + if phase_index < client.boss_def.phases.size(): + phase_name = client.boss_def.phases[phase_index].name + _canvas.draw_string(ThemeDB.fallback_font, pos + Vector2(0.0, -6.0), + "%s -- %s" % [client.boss_def.display_name, phase_name], + HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(1.0, 0.75, 0.8)) + + +func _bar(pos: Vector2, frac: float, fill: Color, back: Color) -> void: + _canvas.draw_rect(Rect2(pos, Vector2(BAR_W, BAR_H)), back) + _canvas.draw_rect(Rect2(pos, Vector2(BAR_W * clampf(frac, 0.0, 1.0), BAR_H)), fill) + + +func flash_hit() -> void: + _hit_flash = 1.0 diff --git a/src/ui/hud.gd.uid b/src/ui/hud.gd.uid new file mode 100644 index 0000000..8924a70 --- /dev/null +++ b/src/ui/hud.gd.uid @@ -0,0 +1 @@ +uid://boq560lv7hkl0 diff --git a/src/ui/main_menu.gd b/src/ui/main_menu.gd new file mode 100644 index 0000000..05b70c2 --- /dev/null +++ b/src/ui/main_menu.gd @@ -0,0 +1,80 @@ +extends Control +## Connect screen. Built in code: it is four controls, and a scene file for it +## would only be one more thing to keep in sync. + +signal join_requested(address: String, port: int) +signal host_requested(port: int) + +var _address: LineEdit +var _port: LineEdit +var _name: LineEdit +var _status: Label + + +func _ready() -> void: + set_anchors_preset(Control.PRESET_FULL_RECT) + var panel := VBoxContainer.new() + panel.set_anchors_preset(Control.PRESET_CENTER) + panel.custom_minimum_size = Vector2(360.0, 0.0) + panel.add_theme_constant_override("separation", 8) + panel.position = Vector2(-180.0, -160.0) + add_child(panel) + + var title := Label.new() + title.text = "TRANSCIENCE" + title.add_theme_font_size_override("font_size", 32) + panel.add_child(title) + + _name = _field(panel, "name", GameOpts.player_name) + _address = _field(panel, "server address", GameOpts.host_address) + _port = _field(panel, "port", str(GameOpts.port)) + + var join := Button.new() + join.text = "Join" + join.pressed.connect(_on_join) + panel.add_child(join) + + var host := Button.new() + host.text = "Host and play (listen server)" + host.pressed.connect(_on_host) + panel.add_child(host) + + var quit := Button.new() + quit.text = "Quit" + quit.pressed.connect(func() -> void: get_tree().quit()) + panel.add_child(quit) + + _status = Label.new() + _status.add_theme_color_override("font_color", Color(1.0, 0.55, 0.5)) + panel.add_child(_status) + + +func _field(parent: Control, label: String, value: String) -> LineEdit: + var l := Label.new() + l.text = label + l.add_theme_font_size_override("font_size", 12) + parent.add_child(l) + var e := LineEdit.new() + e.text = value + parent.add_child(e) + return e + + +func set_status(text: String) -> void: + _status.text = text + + +func _commit_name() -> void: + var clean := _name.text.strip_edges() + if not clean.is_empty(): + GameOpts.player_name = clean + + +func _on_join() -> void: + _commit_name() + join_requested.emit(_address.text.strip_edges(), int(_port.text)) + + +func _on_host() -> void: + _commit_name() + host_requested.emit(int(_port.text)) diff --git a/src/ui/main_menu.gd.uid b/src/ui/main_menu.gd.uid new file mode 100644 index 0000000..1fd3df7 --- /dev/null +++ b/src/ui/main_menu.gd.uid @@ -0,0 +1 @@ +uid://d30lplh710ji8 diff --git a/src/view/bullet_renderer.gd b/src/view/bullet_renderer.gd new file mode 100644 index 0000000..3ad8bbc --- /dev/null +++ b/src/view/bullet_renderer.gd @@ -0,0 +1,60 @@ +extends MultiMeshInstance2D +## Draws the whole bullet field in one draw call. +## +## A bullet hell routinely has a thousand bullets on screen. One Node2D each +## would cost more than the simulation does, so the renderer reads the client's +## [BulletPool] arrays directly and pushes them into a MultiMesh. The sprite is +## generated at runtime, so the project needs no art to run. + +const TEXTURE_SIZE := 32 + +var _colors := { + SimConfig.KIND_PLAYER_SHOT: Color(0.45, 0.95, 1.0), + SimConfig.KIND_ORB: Color(1.0, 0.35, 0.75), + SimConfig.KIND_NEEDLE: Color(1.0, 0.85, 0.3), + SimConfig.KIND_HEAVY: Color(1.0, 0.45, 0.2), +} + + +func _ready() -> void: + texture = _make_dot_texture() + var mm := MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_2D + mm.use_colors = true + var quad := QuadMesh.new() + quad.size = Vector2.ONE + mm.mesh = quad + mm.instance_count = SimConfig.MAX_BULLETS + mm.visible_instance_count = 0 + multimesh = mm + + +## A soft-edged disc with a bright core, so overlapping bullets stay readable. +func _make_dot_texture() -> ImageTexture: + var img := Image.create(TEXTURE_SIZE, TEXTURE_SIZE, false, Image.FORMAT_RGBA8) + var centre := Vector2(TEXTURE_SIZE, TEXTURE_SIZE) * 0.5 + for y in TEXTURE_SIZE: + for x in TEXTURE_SIZE: + var d := Vector2(x + 0.5, y + 0.5).distance_to(centre) / (TEXTURE_SIZE * 0.5) + var alpha := clampf(1.0 - d, 0.0, 1.0) + alpha = pow(alpha, 0.6) + var core := clampf(1.0 - d * 1.9, 0.0, 1.0) + img.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha).lerp(Color.WHITE, core * 0.8)) + return ImageTexture.create_from_image(img) + + +func render_pool(pool: BulletPool) -> void: + var mm := multimesh + var n := 0 + for i in pool.high_water: + if pool.alive[i] == 0: + continue + if n >= SimConfig.MAX_BULLETS: + break + # The texture's visible disc is about 80% of the quad, so scale up a + # little to make the drawn radius match the radius used for hits. + var d: float = pool.radius[i] * 2.5 + mm.set_instance_transform_2d(n, Transform2D(0.0, Vector2(d, d), 0.0, pool.pos[i])) + mm.set_instance_color(n, _colors.get(pool.kind[i], Color.WHITE)) + n += 1 + mm.visible_instance_count = n diff --git a/src/view/bullet_renderer.gd.uid b/src/view/bullet_renderer.gd.uid new file mode 100644 index 0000000..4141243 --- /dev/null +++ b/src/view/bullet_renderer.gd.uid @@ -0,0 +1 @@ +uid://dmjyy62xxru42 diff --git a/src/view/game_scene.gd b/src/view/game_scene.gd new file mode 100644 index 0000000..c8724d4 --- /dev/null +++ b/src/view/game_scene.gd @@ -0,0 +1,29 @@ +extends Node2D +## Root of the playable client scene. Wires the view and HUD to whatever client +## runtime [Net] currently has, and survives that runtime being null (during +## connection) or replaced (on reconnect). + +@onready var world_view: Node2D = $WorldView +@onready var hud: CanvasLayer = $HUD + +var _bound: ClientRuntime = null + + +func _ready() -> void: + world_view.position = get_viewport_rect().size * 0.5 + get_viewport().size_changed.connect(_recentre) + + +func _recentre() -> void: + world_view.position = get_viewport_rect().size * 0.5 + + +func _process(_delta: float) -> void: + if Net.client != _bound: + _bound = Net.client + if _bound != null and not _bound.local_hit.is_connected(_on_local_hit): + _bound.local_hit.connect(_on_local_hit) + + +func _on_local_hit(_damage: int) -> void: + hud.flash_hit() diff --git a/src/view/game_scene.gd.uid b/src/view/game_scene.gd.uid new file mode 100644 index 0000000..c4f3094 --- /dev/null +++ b/src/view/game_scene.gd.uid @@ -0,0 +1 @@ +uid://b0xkfsnpm1xe1 diff --git a/src/view/world_view.gd b/src/view/world_view.gd new file mode 100644 index 0000000..49febf8 --- /dev/null +++ b/src/view/world_view.gd @@ -0,0 +1,119 @@ +extends Node2D +## Draws everything that is not a bullet. There are only a handful of actors, so +## immediate-mode [method CanvasItem.draw_circle] is cheaper than keeping nodes +## in sync with a replicated world. +## +## The view is strictly read-only: it never advances the simulation and never +## decides anything. Every number it draws came from the server. + +const COL_ARENA := Color(0.22, 0.24, 0.34) +const COL_LOCAL := Color(0.5, 1.0, 0.8) +const COL_REMOTE := Color(0.55, 0.75, 1.0) +const COL_DEAD := Color(0.4, 0.4, 0.45, 0.5) +const COL_PORTAL := Color(0.5, 0.9, 1.0) +const ENEMY_COLORS := [ + Color(0.95, 0.55, 0.55), # drifter + Color(0.85, 0.7, 0.35), # turret + Color(0.9, 0.4, 0.9), # stalker + Color(0.5, 0.55, 0.6), # dummy +] + +@onready var bullets: MultiMeshInstance2D = $Bullets + +var client: ClientRuntime = null + + +func _process(_delta: float) -> void: + client = Net.client + if client != null: + bullets.render_pool(client.world.pool) + queue_redraw() + + +func _draw() -> void: + _draw_arena() + if client == null: + return + if client.instance_kind == Protocol.InstanceKind.LOBBY: + _draw_portal() + for e in client.enemies(): + _draw_enemy(e) + _draw_boss() + for p in client.remote_players(): + _draw_remote_player(p) + _draw_local_player() + + +func _draw_arena() -> void: + var h := SimConfig.ARENA_HALF + draw_rect(Rect2(-h, h * 2.0), COL_ARENA, false, 2.0) + # Faint grid, purely so movement reads against a background. + var step := 80.0 + var faint := Color(COL_ARENA, 0.25) + var x := -h.x + step + while x < h.x: + draw_line(Vector2(x, -h.y), Vector2(x, h.y), faint, 1.0) + x += step + var y := -h.y + step + while y < h.y: + draw_line(Vector2(-h.x, y), Vector2(h.x, y), faint, 1.0) + y += step + + +func _draw_portal() -> void: + var pulse := 0.5 + 0.5 * sin(float(Time.get_ticks_msec()) * 0.003) + draw_arc(SimConfig.PORTAL_POS, SimConfig.PORTAL_RADIUS, 0.0, TAU, 48, + Color(COL_PORTAL, 0.4 + 0.4 * pulse), 3.0) + draw_circle(SimConfig.PORTAL_POS, SimConfig.PORTAL_RADIUS * 0.25, + Color(COL_PORTAL, 0.25 + 0.25 * pulse)) + + +func _draw_enemy(e: Dictionary) -> void: + var col: Color = ENEMY_COLORS[clampi(int(e["visual"]), 0, ENEMY_COLORS.size() - 1)] + var r: float = e["radius"] + draw_circle(e["pos"], r, Color(col, 0.35)) + draw_arc(e["pos"], r, 0.0, TAU, 24, col, 2.0) + + +func _draw_boss() -> void: + var b := client.boss_state() + if b.is_empty(): + return + var r: float = client.boss_def.radius if client.boss_def != null else 42.0 + var pos: Vector2 = b["pos"] + draw_circle(pos, r, Color(0.9, 0.3, 0.4, 0.25)) + draw_arc(pos, r, 0.0, TAU, 48, Color(1.0, 0.4, 0.5), 3.0) + draw_arc(pos, r + 10.0, 0.0, TAU, 48, Color(1.0, 0.4, 0.5, 0.25), 1.0) + + +func _draw_remote_player(p: Dictionary) -> void: + var alive := (int(p["flags"]) & Protocol.F_ALIVE) != 0 + var col := COL_REMOTE if alive else COL_DEAD + _draw_ship(p["pos"], p["aim"], col, alive) + if (int(p["flags"]) & Protocol.F_ESCAPING) != 0: + _draw_escape_ring(p["pos"], float(p["escape"]), col) + + +func _draw_local_player() -> void: + if not client.my_alive: + _draw_ship(client.predicted_pos, client.aim, COL_DEAD, false) + return + _draw_ship(client.predicted_pos, client.aim, COL_LOCAL, true) + if client.my_escaping: + _draw_escape_ring(client.predicted_pos, client.my_escape, COL_LOCAL) + + +func _draw_ship(pos: Vector2, aim: float, col: Color, alive: bool) -> void: + var r := SimConfig.PLAYER_RADIUS + draw_circle(pos, r, Color(col, 0.4 if alive else 0.2)) + draw_arc(pos, r, 0.0, TAU, 20, col, 2.0) + if alive: + var dir := Vector2.RIGHT.rotated(aim) + draw_line(pos + dir * r, pos + dir * (r + 12.0), col, 2.0) + + +## The escape channel is drawn on the player, not just in the HUD, so other +## players can see someone is about to leave and react. +func _draw_escape_ring(pos: Vector2, progress: float, col: Color) -> void: + draw_arc(pos, SimConfig.PLAYER_RADIUS + 8.0, -PI * 0.5, + -PI * 0.5 + TAU * progress, 32, Color(col, 0.9), 3.0) diff --git a/src/view/world_view.gd.uid b/src/view/world_view.gd.uid new file mode 100644 index 0000000..f5fac85 --- /dev/null +++ b/src/view/world_view.gd.uid @@ -0,0 +1 @@ +uid://dhkx1oxbrhh32 diff --git a/tests/integration/test_dungeon_flow.gd b/tests/integration/test_dungeon_flow.gd new file mode 100644 index 0000000..9621330 --- /dev/null +++ b/tests/integration/test_dungeon_flow.gd @@ -0,0 +1,109 @@ +extends GutTest +## Progression through a whole dungeon instance: forming, two trash waves, the +## boss, and the cleared state that sends the party home. + +var inst: Instance + + +func before_each() -> void: + inst = Instance.make_dungeon(2, 12345) + inst.add_peer(1, "tester") + + +func _step(n: int) -> void: + for _i in n: + inst.step() + + +func _kill_all_enemies() -> void: + for e in inst.world.enemies.values(): + e.alive = false + + +func test_a_forming_dungeon_locks_after_the_window() -> void: + assert_eq(inst.state, Instance.State.FORMING) + _step(SimConfig.DUNGEON_FORMING_TICKS + 2) + assert_eq(inst.state, Instance.State.ACTIVE) + + +func test_a_full_party_locks_the_dungeon_immediately() -> void: + for peer in range(2, 2 + SimConfig.DUNGEON_PARTY_MAX - 1): + inst.add_peer(peer, "p%d" % peer) + _step(2) + assert_eq(inst.state, Instance.State.ACTIVE) + assert_false(inst.accepts_new_party_member()) + + +func test_waves_gate_on_clearing_the_previous_one() -> void: + _step(SimConfig.DUNGEON_FORMING_TICKS + 200) + assert_eq(inst.stage, 0) + var wave_one := inst.world.enemies.size() + assert_gt(wave_one, 0) + + _step(200) + assert_eq(inst.stage, 0, "a slow party must never be overrun by the next wave") + + _kill_all_enemies() + _step(120) + assert_eq(inst.stage, 1) + assert_gt(inst.world.enemies.size(), wave_one) + + +func test_the_boss_arrives_after_the_last_wave() -> void: + _step(SimConfig.DUNGEON_FORMING_TICKS + 200) + for _wave in 2: + _kill_all_enemies() + _step(120) + assert_eq(inst.stage, 2) + assert_not_null(inst.world.boss) + assert_eq(inst.world.boss.def.id, Content.BOSS_WARDEN) + + +func test_killing_the_boss_clears_the_instance() -> void: + _step(SimConfig.DUNGEON_FORMING_TICKS + 200) + for _wave in 2: + _kill_all_enemies() + _step(120) + inst.world.boss.alive = false + _step(5) + assert_eq(inst.state, Instance.State.CLEARED) + # The exit timer has to run down, or the party would never be released. + _step(400) + assert_eq(inst.stage_delay, 0) + + +func test_the_lobby_has_a_portal_and_no_hostiles() -> void: + var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID) + lobby.add_peer(1, "tester") + assert_true(lobby.world.portal_enabled) + _step(1) + for _i in 600: + lobby.step() + assert_eq(lobby.world.pool.live_count, 0, "nothing in the hub may shoot at you") + assert_eq(lobby.world.players[1].hp, SimConfig.PLAYER_MAX_HP) + + +func test_interacting_on_the_portal_asks_for_a_dungeon() -> void: + var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID) + lobby.add_peer(1, "tester") + lobby.world.players[1].pos = SimConfig.PORTAL_POS + var frames: Array[InputFrame] = [ + InputFrame.make(lobby.world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)] + lobby.world.queue_input(1, frames) + lobby.step() + var used := lobby.world.events.filter( + func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.PORTAL_USED) + assert_eq(used.size(), 1) + + +func test_interacting_away_from_the_portal_does_nothing() -> void: + var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID) + lobby.add_peer(1, "tester") + lobby.world.players[1].pos = SimConfig.PORTAL_POS + Vector2(0.0, SimConfig.PORTAL_RADIUS + 50.0) + var frames: Array[InputFrame] = [ + InputFrame.make(lobby.world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)] + lobby.world.queue_input(1, frames) + lobby.step() + var used := lobby.world.events.filter( + func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.PORTAL_USED) + assert_eq(used.size(), 0) diff --git a/tests/integration/test_dungeon_flow.gd.uid b/tests/integration/test_dungeon_flow.gd.uid new file mode 100644 index 0000000..c78cd93 --- /dev/null +++ b/tests/integration/test_dungeon_flow.gd.uid @@ -0,0 +1 @@ +uid://b0sw7d0vhuxyi diff --git a/tests/integration/test_replica_parity.gd b/tests/integration/test_replica_parity.gd new file mode 100644 index 0000000..92710c3 --- /dev/null +++ b/tests/integration/test_replica_parity.gd @@ -0,0 +1,127 @@ +extends GutTest +## The load-bearing assumption of the whole netcode: a client that only receives +## bullet spawn and despawn events can reproduce the server's bullet field +## exactly, by running the same integration. +## +## That is what lets the server send ~30 bytes once per bullet instead of a +## position for every bullet every snapshot. If this test ever fails, bullets on +## screen no longer match the bullets that can kill you, and the fix is not to +## loosen the tolerance -- it is to stop deriving anything the server did not +## send. + +var server: SimWorld +var replica: SimWorld + + +func before_each() -> void: + server = SimWorld.new(99) + server.authoritative = true + replica = SimWorld.new(99) + replica.authoritative = false + + +func _sync_one_tick() -> Array[Dictionary]: + server.step() + var events := server.drain_events() + # Order matters: the replica first advances the bullets it already knows + # about to the server's new tick, then adopts the spawns, which arrive at + # their post-step positions. + replica.step() + for ev in events: + replica.apply_event(ev) + return events + + +func _mismatches() -> Array[String]: + var out: Array[String] = [] + var server_uids := {} + for i in server.pool.high_water: + if server.pool.alive[i] == 0: + continue + var id: int = server.pool.uid[i] + server_uids[id] = true + var j := replica.pool.find_by_uid(id) + if j < 0: + out.append("bullet %d missing from replica" % id) + continue + var d: float = (server.pool.pos[i] as Vector2).distance_to(replica.pool.pos[j]) + if d > 0.01: + out.append("bullet %d off by %.4f px" % [id, d]) + for i in replica.pool.high_water: + if replica.pool.alive[i] == 1 and not server_uids.has(replica.pool.uid[i]): + out.append("bullet %d lingering on replica" % replica.pool.uid[i]) + return out + + +func test_boss_pattern_reproduces_exactly_on_the_replica() -> void: + server.add_player(1, "tester") + server.players[1].pos = Vector2(0.0, 220.0) + server.spawn_boss(Content.warden()) + for _i in 900: + _sync_one_tick() + assert_gt(server.pool.live_count, 30, "the fight should be producing bullets") + assert_eq(_mismatches(), [] as Array[String]) + + +func test_enemy_patterns_reproduce_exactly_on_the_replica() -> void: + server.add_player(1, "tester") + server.players[1].pos = Vector2(80.0, 200.0) + server.spawn_enemy(Content.turret(), Vector2(-200.0, -100.0)) + server.spawn_enemy(Content.turret(), Vector2(200.0, -100.0), 40) + server.spawn_enemy(Content.drifter(), Vector2(0.0, -200.0)) + for _i in 900: + _sync_one_tick() + assert_gt(server.pool.live_count, 5) + assert_eq(_mismatches(), [] as Array[String]) + + +func test_bullets_killed_by_a_hit_are_removed_from_the_replica() -> void: + # A stationary player soaking fire produces plenty of early despawns, which + # are the one thing the replica cannot derive on its own. + server.add_player(1, "tester") + # Just below the boss, inside the ring pattern rather than at its centre. + server.players[1].pos = Vector2(0.0, 20.0) + server.players[1].iframes = 0 + server.spawn_boss(Content.warden()) + var hits := 0 + for _i in 900: + for ev in _sync_one_tick(): + if int(ev["t"]) == SimEvent.Type.PLAYER_HIT: + hits += 1 + assert_gt(hits, 0, "the test needs the player to actually get hit") + assert_eq(_mismatches(), [] as Array[String]) + + +func test_the_replica_never_invents_a_bullet() -> void: + # Same enemies, but the replica is told nothing at all. It must stay empty: + # emitters and AI are server-side only. + replica.spawn_enemy(Content.turret(), Vector2.ZERO) + replica.spawn_boss(Content.warden()) + for _i in 600: + replica.step() + assert_eq(replica.pool.live_count, 0) + + +func test_full_event_stream_survives_the_wire_format() -> void: + # Same as the parity test, but every event is encoded and decoded on the way, + # so a codec bug cannot hide behind in-process dictionaries. + server.add_player(1, "tester") + server.players[1].pos = Vector2(0.0, 220.0) + server.spawn_boss(Content.warden()) + for _i in 600: + server.step() + var packet := NetCodec.decode_events( + NetCodec.encode_events(server.tick, server.drain_events())) + replica.step() + for ev: Dictionary in packet["events"]: + replica.apply_event(ev) + # float32 on the wire, so allow a hair more slack than the in-process test. + var bad := 0 + for i in server.pool.high_water: + if server.pool.alive[i] == 0: + continue + var j := replica.pool.find_by_uid(server.pool.uid[i]) + if j < 0 or (server.pool.pos[i] as Vector2).distance_to(replica.pool.pos[j]) > 1.0: + bad += 1 + assert_eq(bad, 0, "%d of %d bullets diverged through the codec" + % [bad, server.pool.live_count]) diff --git a/tests/integration/test_replica_parity.gd.uid b/tests/integration/test_replica_parity.gd.uid new file mode 100644 index 0000000..80ca1d7 --- /dev/null +++ b/tests/integration/test_replica_parity.gd.uid @@ -0,0 +1 @@ +uid://dvqtvvt3chqcs diff --git a/tests/unit/test_boss.gd b/tests/unit/test_boss.gd new file mode 100644 index 0000000..0575410 --- /dev/null +++ b/tests/unit/test_boss.gd @@ -0,0 +1,115 @@ +extends GutTest +## The boss format is the thing the MVP has to prove is reusable, so these tests +## are written against [BossDef]/[BossPhase] rather than against the Warden -- +## a second boss should pass them unchanged. + +var world: SimWorld + + +func before_each() -> void: + world = SimWorld.new(3) + world.add_player(1, "tester") + world.players[1].pos = Vector2(0.0, 200.0) + + +func _spawn_warden() -> SimBoss: + return world.spawn_boss(Content.warden()) + + +func test_phase_selection_follows_hp_fraction() -> void: + var def := Content.warden() + assert_eq(def.phase_index_for(1.0), 0) + assert_eq(def.phase_index_for(0.9), 0) + assert_eq(def.phase_index_for(0.5), 1) + assert_eq(def.phase_index_for(0.3), 2) + assert_eq(def.phase_index_for(0.05), 3) + + +func test_boss_advances_phase_when_damaged() -> void: + var boss := _spawn_warden() + world.step() + assert_eq(boss.phase_index, 0) + boss.hp = int(float(boss.def.max_hp) * 0.5) + world.step() + assert_eq(boss.phase_index, 1) + assert_eq(boss.phase_tick, 1, "entering a phase restarts its timeline") + + +func test_phase_change_is_announced() -> void: + var boss := _spawn_warden() + world.step() + world.drain_events() + boss.hp = int(float(boss.def.max_hp) * 0.3) + world.step() + var phases := world.events.filter( + func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.BOSS_PHASE) + assert_eq(phases.size(), 1) + assert_eq(int(phases[0]["phase"]), 2) + + +func test_telegraph_holds_fire_before_the_phase_starts() -> void: + var boss := _spawn_warden() + var telegraph: int = boss.def.phases[0].telegraph_ticks + for _i in telegraph: + world.step() + assert_eq(world.pool.live_count, 0, "the telegraph window must be silent") + world.step() + assert_gt(world.pool.live_count, 0, "the pattern must start once telegraphed") + + +func test_the_boss_actually_fills_the_arena() -> void: + _spawn_warden() + for _i in 600: + world.step() + assert_gt(world.pool.live_count, 40, + "a boss phase should keep a real bullet field in the air") + + +func test_armour_multiplier_scales_incoming_damage() -> void: + var def := Content.warden() + def.phases[0].damage_taken_mult = 0.5 + var boss := world.spawn_boss(def) + boss.pos = Vector2.ZERO + world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 4.0, 60, 100, + SimConfig.TEAM_PLAYER, SimConfig.KIND_PLAYER_SHOT) + world.step() + assert_eq(boss.hp, def.max_hp - 50) + + +func test_boss_death_is_announced_once() -> void: + var boss := _spawn_warden() + boss.pos = Vector2.ZERO + boss.hp = 10 + world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 4.0, 60, 100, + SimConfig.TEAM_PLAYER, SimConfig.KIND_PLAYER_SHOT) + world.step() + assert_false(boss.alive) + var deaths := world.events.filter( + func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.BOSS_DIED) + assert_eq(deaths.size(), 1) + + +## The point of the format: a boss built from scratch in a test, with no code +## anywhere in the simulation that knows about it, has to work. +func test_a_brand_new_boss_needs_no_engine_changes() -> void: + var ring := RingEmitter.new() + ring.interval = 20 + ring.count = 6 + + var phase := BossPhase.new() + phase.name = "Only Phase" + phase.enter_at_hp_fraction = 1.0 + phase.telegraph_ticks = 0 + phase.loop_ticks = 100 + phase.emitters = [ring] + + var def := BossDef.new() + def.id = &"test_boss" + def.display_name = "Test Boss" + def.max_hp = 500 + def.radius = 30.0 + def.phases = [phase] + + world.spawn_boss(def) + world.step() + assert_eq(world.pool.live_count, 6) diff --git a/tests/unit/test_boss.gd.uid b/tests/unit/test_boss.gd.uid new file mode 100644 index 0000000..8a3aaec --- /dev/null +++ b/tests/unit/test_boss.gd.uid @@ -0,0 +1 @@ +uid://dqau1uvkshq7r diff --git a/tests/unit/test_bullet_pool.gd b/tests/unit/test_bullet_pool.gd new file mode 100644 index 0000000..0fb2056 --- /dev/null +++ b/tests/unit/test_bullet_pool.gd @@ -0,0 +1,94 @@ +extends GutTest +## The pool is the hottest data structure in the game and the one place a leak +## would end the run, so its slot bookkeeping is tested directly. + +var pool: BulletPool + + +func before_each() -> void: + pool = BulletPool.new() + + +func _spawn(p := Vector2.ZERO, v := Vector2(100, 0), life := 60) -> int: + return pool.spawn(p, v, 5.0, life, 10, SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + + +func test_spawn_assigns_unique_ids() -> void: + var a := _spawn() + var b := _spawn() + assert_ne(pool.uid[a], pool.uid[b]) + assert_eq(pool.live_count, 2) + + +func test_despawned_slots_are_reused() -> void: + var a := _spawn() + pool.despawn(a) + assert_eq(pool.live_count, 0) + var b := _spawn() + assert_eq(b, a, "the free list should hand back the slot just released") + assert_eq(pool.high_water, 1, "reuse must not grow the scanned range") + + +func test_double_despawn_does_not_corrupt_the_count() -> void: + var a := _spawn() + pool.despawn(a) + pool.despawn(a) + assert_eq(pool.live_count, 0) + + +func test_step_integrates_velocity() -> void: + var a := _spawn(Vector2.ZERO, Vector2(600, 0)) + pool.step() + assert_almost_eq(pool.pos[a].x, 600.0 * SimConfig.TICK_DELTA, 0.001) + + +func test_turn_curves_the_bullet() -> void: + var a := pool.spawn(Vector2.ZERO, Vector2(100, 0), 5.0, 60, 10, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 0.0, deg_to_rad(90.0)) + pool.step() + assert_almost_eq(pool.vel[a].angle(), deg_to_rad(90.0), 0.001) + + +func test_bullets_expire_at_end_of_life() -> void: + var a := _spawn(Vector2.ZERO, Vector2.ZERO, 3) + for _i in 3: + pool.step() + assert_eq(pool.alive[a], 0) + assert_eq(pool.live_count, 0) + + +func test_bullets_leaving_the_arena_are_culled() -> void: + var start := Vector2(SimConfig.ARENA_HALF.x, 0.0) + var a := _spawn(start, Vector2(100000, 0), 600) + pool.step() + assert_eq(pool.alive[a], 0, "a bullet past the cull margin must not linger") + + +func test_pool_saturation_returns_minus_one_rather_than_growing() -> void: + for _i in SimConfig.MAX_BULLETS: + _spawn(Vector2.ZERO, Vector2.ZERO, 10000) + assert_eq(pool.live_count, SimConfig.MAX_BULLETS) + assert_eq(_spawn(), -1) + + +func test_advance_slot_matches_repeated_steps() -> void: + var a := pool.spawn(Vector2.ZERO, Vector2(150, 40), 5.0, 300, 10, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 30.0, 0.02) + var other := BulletPool.new() + var b := other.spawn(Vector2.ZERO, Vector2(150, 40), 5.0, 300, 10, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 30.0, 0.02) + pool.advance_slot(a, 12) + for _i in 12: + other.step() + # Client catch-up must land on exactly the server's integration, or bullets + # would drift a little further out of place with every packet. + assert_almost_eq(pool.pos[a].x, other.pos[b].x, 0.0001) + assert_almost_eq(pool.pos[a].y, other.pos[b].y, 0.0001) + + +func test_find_by_uid_only_matches_live_bullets() -> void: + var a := _spawn() + var id: int = pool.uid[a] + assert_eq(pool.find_by_uid(id), a) + pool.despawn(a) + assert_eq(pool.find_by_uid(id), -1) diff --git a/tests/unit/test_bullet_pool.gd.uid b/tests/unit/test_bullet_pool.gd.uid new file mode 100644 index 0000000..839bf60 --- /dev/null +++ b/tests/unit/test_bullet_pool.gd.uid @@ -0,0 +1 @@ +uid://d1ahm5mut1bdh diff --git a/tests/unit/test_emergency_escape.gd b/tests/unit/test_emergency_escape.gd new file mode 100644 index 0000000..d3bd5a8 --- /dev/null +++ b/tests/unit/test_emergency_escape.gd @@ -0,0 +1,70 @@ +extends GutTest +## The escape button is the player's safety valve, and it is also the one input +## with a real consequence attached, so its timing is server-owned end to end. + +var world: SimWorld +const PEER := 3 + + +func before_each() -> void: + world = SimWorld.new(1) + world.add_player(PEER, "tester") + world.players[PEER].iframes = 0 + + +func _hold_escape(ticks: int) -> void: + for _i in ticks: + var frames: Array[InputFrame] = [ + InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_ESCAPE)] + world.queue_input(PEER, frames) + world.step() + + +func _events_of(type: int) -> Array: + return world.events.filter(func(e: Dictionary) -> bool: return int(e["t"]) == type) + + +func test_escape_takes_the_full_channel_time() -> void: + _hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS - 1) + assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 0, + "the escape must not complete early") + _hold_escape(1) + assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 1) + + +func test_releasing_the_button_cancels_the_channel() -> void: + _hold_escape(60) + assert_gt(world.players[PEER].escape_ticks, 0) + var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, 0)] + world.queue_input(PEER, frames) + world.step() + assert_eq(world.players[PEER].escape_ticks, 0) + assert_eq(_events_of(SimEvent.Type.ESCAPE_CANCELLED).size(), 1) + + +func test_taking_damage_cancels_the_channel() -> void: + _hold_escape(60) + world.pool.spawn(world.players[PEER].pos, Vector2.ZERO, 6.0, 60, 10, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + world.step() + assert_eq(world.players[PEER].escape_ticks, 0, + "escaping under fire has to be a real risk, not a free exit") + assert_gt(_events_of(SimEvent.Type.ESCAPE_CANCELLED).size(), 0) + + +func test_a_cancelled_channel_restarts_from_zero() -> void: + _hold_escape(120) + var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, 0)] + world.queue_input(PEER, frames) + world.step() + _hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS - 1) + assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 0, + "progress must not carry over from an interrupted channel") + + +func test_escape_completion_never_reaches_the_client_directly() -> void: + _hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS) + var packet := NetCodec.decode_events(NetCodec.encode_events(world.tick, world.events)) + for ev: Dictionary in packet["events"]: + assert_ne(int(ev["t"]), SimEvent.Type.ESCAPE_COMPLETED, + "the transfer is the server's decision; clients only see the outcome") diff --git a/tests/unit/test_emergency_escape.gd.uid b/tests/unit/test_emergency_escape.gd.uid new file mode 100644 index 0000000..97d7bd3 --- /dev/null +++ b/tests/unit/test_emergency_escape.gd.uid @@ -0,0 +1 @@ +uid://chkd5b3cni3wu diff --git a/tests/unit/test_emitters.gd b/tests/unit/test_emitters.gd new file mode 100644 index 0000000..977e4f5 --- /dev/null +++ b/tests/unit/test_emitters.gd @@ -0,0 +1,147 @@ +extends GutTest +## Emitters are the authoring surface for every enemy and boss, so their shapes +## are asserted directly rather than eyeballed in game. + +var pool: BulletPool +var ctx: EmitContext + + +func before_each() -> void: + pool = BulletPool.new() + ctx = EmitContext.new() + ctx.pool = pool + ctx.origin = Vector2.ZERO + ctx.rng = RandomNumberGenerator.new() + ctx.rng.seed = 1 + + +func _angles() -> Array[float]: + var out: Array[float] = [] + for i in pool.high_water: + if pool.alive[i] == 1: + out.append(pool.vel[i].angle()) + return out + + +func test_ring_emits_the_requested_count() -> void: + var e := RingEmitter.new() + e.count = 8 + e.fire(ctx) + assert_eq(pool.live_count, 8) + + +func test_closed_ring_is_evenly_spaced_with_no_duplicate_at_the_seam() -> void: + var e := RingEmitter.new() + e.count = 8 + e.arc_deg = 360.0 + e.spin_per_shot_deg = 0.0 + e.fire(ctx) + var angles := _angles() + angles.sort() + for i in range(1, angles.size()): + assert_almost_eq(angles[i] - angles[i - 1], TAU / 8.0, 0.001) + + +func test_spin_advances_with_shot_index() -> void: + var e := RingEmitter.new() + e.count = 1 + e.spin_per_shot_deg = 30.0 + e.fire(ctx) + var first := _angles()[0] + pool.clear() + ctx.shot_index = 1 + e.fire(ctx) + assert_almost_eq(_angles()[0] - first, deg_to_rad(30.0), 0.001) + + +func test_should_fire_respects_interval_and_window() -> void: + var e := RingEmitter.new() + e.start_tick = 10 + e.end_tick = 40 + e.interval = 10 + assert_false(e.should_fire(9), "not armed before start_tick") + assert_true(e.should_fire(10)) + assert_false(e.should_fire(15)) + assert_true(e.should_fire(30)) + assert_false(e.should_fire(50), "not armed after end_tick") + + +func test_shot_index_matches_the_number_of_shots_so_far() -> void: + var e := RingEmitter.new() + e.start_tick = 10 + e.interval = 10 + assert_eq(e.shot_index_at(10), 0) + assert_eq(e.shot_index_at(30), 2) + + +func test_aimed_spread_centres_on_the_target() -> void: + var e := AimedSpreadEmitter.new() + e.count = 3 + e.spread_deg = 30.0 + ctx.has_target = true + ctx.target = Vector2(100.0, 0.0) + e.fire(ctx) + var angles := _angles() + angles.sort() + assert_eq(angles.size(), 3) + assert_almost_eq(angles[1], 0.0, 0.001, "the middle shot must point at the target") + assert_almost_eq(angles[2] - angles[0], deg_to_rad(30.0), 0.001) + + +func test_aimed_spread_falls_back_when_nobody_is_alive() -> void: + var e := AimedSpreadEmitter.new() + e.count = 1 + ctx.has_target = false + e.fire(ctx) + assert_eq(pool.live_count, 1, "an emitter with no target must still be safe to fire") + + +func test_wall_leaves_a_gap_of_the_requested_width() -> void: + var e := WallGapEmitter.new() + e.count = 20 + e.gap_width = 3 + e.fire(ctx) + assert_eq(pool.live_count, 17) + + +func test_wall_gap_slides_between_shots() -> void: + var e := WallGapEmitter.new() + e.count = 20 + e.gap_width = 3 + e.gap_step = 5 + e.fire(ctx) + var first_xs: Array = [] + for i in pool.high_water: + if pool.alive[i] == 1: + first_xs.append(snappedf(pool.pos[i].x, 0.01)) + pool.clear() + ctx.shot_index = 1 + e.fire(ctx) + var second_xs: Array = [] + for i in pool.high_water: + if pool.alive[i] == 1: + second_xs.append(snappedf(pool.pos[i].x, 0.01)) + assert_ne(first_xs, second_xs, "the safe lane must move so it cannot be camped") + + +func test_arc_sweep_emits_every_arm() -> void: + var e := ArcSweepEmitter.new() + e.arms = 3 + e.bullets_per_arm = 4 + e.fire(ctx) + assert_eq(pool.live_count, 12) + + +func test_arc_sweep_angle_changes_over_the_phase() -> void: + var e := ArcSweepEmitter.new() + e.arms = 1 + e.bullets_per_arm = 1 + e.sweep_period = 4.0 + e.sweep_deg = 90.0 + ctx.local_tick = 0 + e.fire(ctx) + var a0 := _angles()[0] + pool.clear() + ctx.local_tick = 30 + e.fire(ctx) + assert_ne(snappedf(a0, 0.001), snappedf(_angles()[0], 0.001)) diff --git a/tests/unit/test_emitters.gd.uid b/tests/unit/test_emitters.gd.uid new file mode 100644 index 0000000..f01bb0d --- /dev/null +++ b/tests/unit/test_emitters.gd.uid @@ -0,0 +1 @@ +uid://bc7gkxydar6fa diff --git a/tests/unit/test_input_frame.gd b/tests/unit/test_input_frame.gd new file mode 100644 index 0000000..560a37e --- /dev/null +++ b/tests/unit/test_input_frame.gd @@ -0,0 +1,43 @@ +extends GutTest +## The input packet is the only thing a client can put on the wire, so its +## bounds are a security surface as much as a format. + + +func _round_trip(f: InputFrame) -> InputFrame: + var b := StreamPeerBuffer.new() + b.big_endian = false + f.write(b) + b.seek(0) + return InputFrame.read(b) + + +func test_round_trip_preserves_tick_and_buttons() -> void: + var f := InputFrame.make(123456, Vector2(0.5, -0.25), 1.75, + InputFrame.BTN_FIRE | InputFrame.BTN_ESCAPE) + var out := _round_trip(f) + assert_eq(out.tick, 123456) + assert_true(out.pressed(InputFrame.BTN_FIRE)) + assert_true(out.pressed(InputFrame.BTN_ESCAPE)) + assert_false(out.pressed(InputFrame.BTN_INTERACT)) + + +func test_move_survives_quantisation_within_tolerance() -> void: + var out := _round_trip(InputFrame.make(1, Vector2(0.5, -0.25), 0.0, 0)) + assert_almost_eq(out.move.x, 0.5, 0.01) + assert_almost_eq(out.move.y, -0.25, 0.01) + + +func test_aim_survives_quantisation() -> void: + var out := _round_trip(InputFrame.make(1, Vector2.ZERO, 2.5, 0)) + assert_almost_eq(out.aim, 2.5, 0.001) + + +func test_oversized_move_cannot_be_expressed_on_the_wire() -> void: + var out := _round_trip(InputFrame.make(1, Vector2(50.0, 50.0), 0.0, 0)) + assert_lt(out.move.length(), 1.45, "the byte encoding must cap the move vector") + + +func test_frame_is_exactly_the_declared_size() -> void: + var b := StreamPeerBuffer.new() + InputFrame.make(1, Vector2.ONE, 1.0, 7).write(b) + assert_eq(b.data_array.size(), InputFrame.SIZE) diff --git a/tests/unit/test_input_frame.gd.uid b/tests/unit/test_input_frame.gd.uid new file mode 100644 index 0000000..a9e074b --- /dev/null +++ b/tests/unit/test_input_frame.gd.uid @@ -0,0 +1 @@ +uid://dyg6ud6qm24gw diff --git a/tests/unit/test_movement.gd b/tests/unit/test_movement.gd new file mode 100644 index 0000000..58a0454 --- /dev/null +++ b/tests/unit/test_movement.gd @@ -0,0 +1,39 @@ +extends GutTest +## Movement is the one function the client is allowed to run ahead of the +## server, so client prediction is only correct while these hold. + + +func test_diagonal_is_not_faster_than_cardinal() -> void: + var straight := Movement.step_player(Vector2.ZERO, Vector2(1, 0), SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + var diagonal := Movement.step_player(Vector2.ZERO, Vector2(1, 1), SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + assert_almost_eq(diagonal.length(), straight.length(), 0.001, + "a diagonal must cover the same distance as a cardinal move") + + +func test_oversized_input_vector_is_clamped() -> void: + # The wire format cannot express this, but a patched client could try. + var cheated := Movement.step_player(Vector2.ZERO, Vector2(1000, 0), SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + var honest := Movement.step_player(Vector2.ZERO, Vector2(1, 0), SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + assert_eq(cheated, honest, "an over-long move vector must buy no extra speed") + + +func test_speed_matches_config() -> void: + var moved := Movement.step_player(Vector2.ZERO, Vector2.RIGHT, SimConfig.PLAYER_SPEED, SimConfig.ARENA_HALF) + assert_almost_eq(moved.x, SimConfig.PLAYER_SPEED * SimConfig.TICK_DELTA, 0.001) + + +func test_player_is_clamped_to_the_arena() -> void: + var far := Vector2(SimConfig.ARENA_HALF.x - 1.0, 0.0) + var out := Movement.step_player(far, Vector2.RIGHT, 10000.0, SimConfig.ARENA_HALF) + assert_almost_eq(out.x, SimConfig.ARENA_HALF.x, 0.001) + + +func test_circles_overlap_at_the_boundary() -> void: + assert_true(Movement.circles_overlap(Vector2.ZERO, 5.0, Vector2(9.9, 0.0), 5.0)) + assert_false(Movement.circles_overlap(Vector2.ZERO, 5.0, Vector2(10.1, 0.0), 5.0)) + + +func test_outside_arena_respects_the_cull_margin() -> void: + var edge := SimConfig.ARENA_HALF.x + SimConfig.BULLET_CULL_MARGIN + assert_false(Movement.outside_arena(Vector2(edge - 1.0, 0.0))) + assert_true(Movement.outside_arena(Vector2(edge + 1.0, 0.0))) diff --git a/tests/unit/test_movement.gd.uid b/tests/unit/test_movement.gd.uid new file mode 100644 index 0000000..c168cf9 --- /dev/null +++ b/tests/unit/test_movement.gd.uid @@ -0,0 +1 @@ +uid://bnhdou74qn25a diff --git a/tests/unit/test_net_codec.gd b/tests/unit/test_net_codec.gd new file mode 100644 index 0000000..2b1665c --- /dev/null +++ b/tests/unit/test_net_codec.gd @@ -0,0 +1,107 @@ +extends GutTest +## Codec round-trips. A field that encodes but decodes wrong shows up as a +## mysterious gameplay bug three layers away, so it is pinned down here. + +var world: SimWorld + + +func before_each() -> void: + world = SimWorld.new(7) + var p := world.add_player(42, "ada") + p.pos = Vector2(120.5, -64.25) + p.aim = 1.25 + p.hp = 73 + p.escape_ticks = 90 + p.last_input_tick = 555 + world.spawn_enemy(Content.turret(), Vector2(-200.0, 100.0)) + world.spawn_boss(Content.warden()) + world.tick = 9001 + + +func test_snapshot_round_trips_player_state() -> void: + var snap := NetCodec.decode_snapshot(NetCodec.encode_snapshot(world)) + assert_eq(int(snap["tick"]), 9001) + var rec: Dictionary = snap["players"][0] + assert_eq(int(rec["peer"]), 42) + assert_almost_eq((rec["pos"] as Vector2).x, 120.5, 0.01) + assert_almost_eq((rec["pos"] as Vector2).y, -64.25, 0.01) + assert_almost_eq(float(rec["aim"]), 1.25, 0.001) + assert_eq(int(rec["hp"]), 73) + assert_eq(int(rec["last_input_tick"]), 555) + assert_true((int(rec["flags"]) & Protocol.F_ALIVE) != 0) + assert_true((int(rec["flags"]) & Protocol.F_ESCAPING) != 0) + assert_almost_eq(float(rec["escape"]), 90.0 / float(SimConfig.ESCAPE_CHANNEL_TICKS), 0.01) + + +func test_snapshot_carries_enemy_radius_for_late_joiners() -> void: + var snap := NetCodec.decode_snapshot(NetCodec.encode_snapshot(world)) + var e: Dictionary = snap["enemies"][0] + assert_almost_eq(float(e["radius"]), Content.turret().radius, 0.5) + assert_eq(int(e["visual"]), Content.turret().visual) + + +func test_snapshot_carries_the_boss() -> void: + var snap := NetCodec.decode_snapshot(NetCodec.encode_snapshot(world)) + assert_not_null(snap["boss"]) + assert_eq(int(snap["boss"]["hp"]), Content.warden().max_hp) + + +func test_dead_enemies_are_not_sent() -> void: + for e in world.enemies.values(): + e.alive = false + var snap := NetCodec.decode_snapshot(NetCodec.encode_snapshot(world)) + assert_eq((snap["enemies"] as Array).size(), 0) + + +func test_event_round_trip_preserves_bullet_spawn() -> void: + var ev := { + "t": SimEvent.Type.BULLET_SPAWN, "uid": 8123, + "pos": Vector2(10.0, -20.0), "vel": Vector2(0.0, 150.0), + "r": 7.5, "life": 300, "kind": SimConfig.KIND_NEEDLE, + "team": SimConfig.TEAM_ENEMY, "accel": 12.0, "turn": 0.02, + } + var packet := NetCodec.decode_events(NetCodec.encode_events(77, [ev])) + assert_eq(int(packet["tick"]), 77) + var out: Dictionary = packet["events"][0] + assert_eq(int(out["uid"]), 8123) + assert_eq(out["pos"], Vector2(10.0, -20.0)) + assert_eq(out["vel"], Vector2(0.0, 150.0)) + assert_almost_eq(float(out["r"]), 7.5, 0.001) + assert_eq(int(out["life"]), 300) + assert_almost_eq(float(out["accel"]), 12.0, 0.001) + assert_almost_eq(float(out["turn"]), 0.02, 0.0001) + + +func test_server_only_events_never_reach_the_wire() -> void: + var events: Array[Dictionary] = [ + {"t": SimEvent.Type.PORTAL_USED, "peer": 3}, + {"t": SimEvent.Type.ESCAPE_COMPLETED, "peer": 3}, + {"t": SimEvent.Type.PLAYER_DIED, "peer": 3}, + ] + var packet := NetCodec.decode_events(NetCodec.encode_events(1, events)) + assert_eq((packet["events"] as Array).size(), 1, + "instance transfers are the server's business, not the client's") + assert_eq(int(packet["events"][0]["t"]), SimEvent.Type.PLAYER_DIED) + + +func test_input_round_trip() -> void: + var frames: Array[InputFrame] = [ + InputFrame.make(10, Vector2(1, 0), 0.0, InputFrame.BTN_FIRE), + InputFrame.make(11, Vector2(0, 1), 1.0, 0), + ] + var out := NetCodec.decode_inputs(NetCodec.encode_inputs(frames)) + assert_eq(out.size(), 2) + assert_eq(out[0].tick, 10) + assert_eq(out[1].tick, 11) + + +func test_truncated_input_packet_is_rejected_not_read_past() -> void: + var frames: Array[InputFrame] = [InputFrame.make(10, Vector2.ONE, 0.0, 1)] + var data := NetCodec.encode_inputs(frames) + # Claim three frames but send one. A hostile client will try exactly this. + data[0] = 3 + assert_eq(NetCodec.decode_inputs(data).size(), 0) + + +func test_empty_input_packet_is_safe() -> void: + assert_eq(NetCodec.decode_inputs(PackedByteArray()).size(), 0) diff --git a/tests/unit/test_net_codec.gd.uid b/tests/unit/test_net_codec.gd.uid new file mode 100644 index 0000000..7f72580 --- /dev/null +++ b/tests/unit/test_net_codec.gd.uid @@ -0,0 +1 @@ +uid://bsarugkk427hn diff --git a/tests/unit/test_server_authority.gd b/tests/unit/test_server_authority.gd new file mode 100644 index 0000000..0ba5704 --- /dev/null +++ b/tests/unit/test_server_authority.gd @@ -0,0 +1,138 @@ +extends GutTest +## The security tests. Each one describes something a modified client would try +## and asserts that the authoritative world does not let it happen. +## +## The design intent is that these are boring to write, because the client has +## no message that expresses the cheat in the first place -- it can only send +## intent. These tests pin that property down so a future "just let the client +## send its position, it is simpler" change fails loudly. + +var world: SimWorld +const PEER := 7 + + +func before_each() -> void: + world = SimWorld.new(1) + world.add_player(PEER, "tester") + + +func _send(frame_tick: int, move := Vector2.ZERO, buttons := 0, aim := 0.0) -> void: + var frames: Array[InputFrame] = [InputFrame.make(frame_tick, move, aim, buttons)] + world.queue_input(PEER, frames) + + +## Drive the player for [param ticks] ticks with one fresh input per tick. +func _drive(ticks: int, move := Vector2.ZERO, buttons := 0, aim := 0.0) -> void: + for _i in ticks: + _send(world.tick + 1, move, buttons, aim) + world.step() + + +func test_player_cannot_outrun_the_configured_speed() -> void: + var start: Vector2 = world.players[PEER].pos + _drive(60, Vector2(1.0, 0.0)) + var travelled: float = world.players[PEER].pos.distance_to(start) + assert_almost_eq(travelled, SimConfig.PLAYER_SPEED, 1.0, + "one second of held input must cover exactly one second of movement") + + +func test_replayed_input_is_dropped() -> void: + _drive(5, Vector2.RIGHT) + var pos_after: Vector2 = world.players[PEER].pos + var acked: int = world.players[PEER].last_input_tick + # Re-send an already-consumed tick, the classic replay attack. + _send(acked, Vector2.RIGHT) + assert_eq(world.players[PEER].input_queue.size(), 0) + world.step() + # The held input coasts one more tick, which is expected; what matters is + # that the stale frame did not stack a second move on top of it. + assert_almost_eq(world.players[PEER].pos.distance_to(pos_after), + SimConfig.PLAYER_SPEED * SimConfig.TICK_DELTA, 0.001) + + +func test_input_from_the_far_future_is_dropped() -> void: + _send(world.tick + SimConfig.INPUT_MAX_LEAD + 50, Vector2.RIGHT) + assert_eq(world.players[PEER].input_queue.size(), 0, + "a client cannot buy a head start by claiming a future tick") + + +func test_ancient_input_is_dropped() -> void: + world.tick = 10000 + _send(1, Vector2.RIGHT) + assert_eq(world.players[PEER].input_queue.size(), 0) + + +func test_input_flood_cannot_grow_the_queue_without_bound() -> void: + for i in 500: + _send(world.tick + 1 + i, Vector2.RIGHT) + assert_lte(world.players[PEER].input_queue.size(), SimConfig.INPUT_MAX_AGE, + "a flood of inputs must not become unbounded server memory") + + +func test_fire_rate_is_enforced_by_the_server() -> void: + # Hold fire every single tick; the server still applies its own cooldown. + _drive(60, Vector2.ZERO, InputFrame.BTN_FIRE) + var expected := 60 / SimConfig.PLAYER_FIRE_COOLDOWN + assert_almost_eq(float(world.pool.live_count), float(expected), 2.0, + "holding fire must not fire faster than the cooldown allows") + + +func test_a_starved_player_eventually_stops_moving() -> void: + _drive(3, Vector2.RIGHT) + var pos_at_starve: Vector2 = world.players[PEER].pos + # Send nothing at all for a long time, as a disconnecting client would. + for _i in SimConfig.INPUT_MAX_AGE + 120: + world.step() + var coasted: float = world.players[PEER].pos.distance_to(pos_at_starve) + assert_lt(coasted, SimConfig.PLAYER_SPEED * 1.0, + "a silent client must coast briefly, then stop, not drift forever") + + +func test_enemy_bullets_damage_the_player_and_are_consumed() -> void: + var p: SimPlayer = world.players[PEER] + p.pos = Vector2.ZERO + p.iframes = 0 + world.pool.spawn(Vector2(-1.0, 0.0), Vector2.ZERO, 6.0, 60, 25, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + world.step() + assert_eq(p.hp, SimConfig.PLAYER_MAX_HP - 25) + assert_eq(world.pool.live_count, 0, "a bullet that hits must be consumed") + + +func test_invulnerability_frames_stop_a_second_hit() -> void: + var p: SimPlayer = world.players[PEER] + p.pos = Vector2.ZERO + p.iframes = 0 + for _i in 2: + world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 6.0, 60, 25, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + world.step() + assert_eq(p.hp, SimConfig.PLAYER_MAX_HP - 25, "two bullets in one tick is still one hit") + + +func test_a_replica_world_never_resolves_a_hit() -> void: + world.authoritative = false + var p: SimPlayer = world.players[PEER] + p.pos = Vector2.ZERO + p.iframes = 0 + world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 6.0, 60, 25, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + world.step() + assert_eq(p.hp, SimConfig.PLAYER_MAX_HP, + "only the server decides damage; a client replica must never apply it") + + +func test_player_death_and_respawn() -> void: + var p: SimPlayer = world.players[PEER] + p.pos = Vector2.ZERO + p.hp = 5 + p.iframes = 0 + world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 6.0, 60, 25, + SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB) + world.step() + assert_false(p.alive) + for _i in SimConfig.PLAYER_RESPAWN_DELAY + 1: + world.step() + assert_true(p.alive) + assert_eq(p.hp, SimConfig.PLAYER_MAX_HP) + assert_eq(p.pos, world.spawn_point) diff --git a/tests/unit/test_server_authority.gd.uid b/tests/unit/test_server_authority.gd.uid new file mode 100644 index 0000000..94864ea --- /dev/null +++ b/tests/unit/test_server_authority.gd.uid @@ -0,0 +1 @@ +uid://mmnj5ebk3h8q diff --git a/tools/bench.gd b/tools/bench.gd new file mode 100644 index 0000000..2557f6c --- /dev/null +++ b/tools/bench.gd @@ -0,0 +1,83 @@ +extends SceneTree +## Headless performance probe for the simulation alone -- no transport, no +## rendering. Answers the only question that decides whether the design holds: +## how many ticks per second can one dedicated server sustain per instance, and +## how big does the bullet field get? +## +## godot --headless --path . --script tools/bench.gd +## godot --headless --path . --script tools/bench.gd -- --players 4 --ticks 5400 + +var players: int = 4 +var ticks: int = 3600 + + +func _init() -> void: + _parse() + print("bench: %d players, %d ticks (%.1f s of play)" + % [players, ticks, float(ticks) / float(SimConfig.TICK_RATE)]) + _run("boss fight", _make_boss_world()) + _run("trash wave", _make_wave_world()) + quit(0) + + +func _parse() -> void: + var argv := OS.get_cmdline_user_args() + var i := 0 + while i < argv.size(): + match argv[i]: + "--players": + i += 1 + players = int(argv[i]) + "--ticks": + i += 1 + ticks = int(argv[i]) + i += 1 + + +func _add_players(world: SimWorld) -> void: + for n in players: + var p := world.add_player(n + 2, "bench%d" % n) + p.pos = Vector2(-200.0 + 100.0 * float(n), 180.0) + + +func _make_boss_world() -> SimWorld: + var world := SimWorld.new(1234) + _add_players(world) + world.spawn_boss(Content.warden()) + return world + + +func _make_wave_world() -> SimWorld: + var world := SimWorld.new(4321) + _add_players(world) + for i in 6: + world.spawn_enemy(Content.turret(), Vector2(-400.0 + 160.0 * float(i), -220.0), i * 20) + for i in 4: + world.spawn_enemy(Content.drifter(), Vector2(-240.0 + 160.0 * float(i), -80.0), i * 25) + return world + + +func _run(label: String, world: SimWorld) -> void: + var peak := 0 + var total := 0 + var start := Time.get_ticks_usec() + for t in ticks: + for peer in world.players: + var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1, + Vector2(cos(float(t) * 0.02), sin(float(t) * 0.031)), + float(t) * 0.05, InputFrame.BTN_FIRE)] + world.queue_input(peer, frames) + world.step() + world.drain_events() + if world.boss != null: + # Sweep hp downwards instead of letting the boss die, so the run + # covers every phase including the heaviest one. + world.boss.hp = int(float(world.boss.def.max_hp) + * (0.95 - 0.9 * float(t) / float(ticks))) + peak = maxi(peak, world.pool.live_count) + total += world.pool.live_count + var elapsed := float(Time.get_ticks_usec() - start) / 1_000_000.0 + var per_tick_ms := elapsed / float(ticks) * 1000.0 + print(" %-12s %7.3f ms/tick peak %4d bullets avg %4d headroom x%.1f" + % [label, per_tick_ms, peak, total / ticks, + (1000.0 / float(SimConfig.TICK_RATE)) / maxf(per_tick_ms, 0.0001)]) diff --git a/tools/bench.gd.uid b/tools/bench.gd.uid new file mode 100644 index 0000000..73659d6 --- /dev/null +++ b/tools/bench.gd.uid @@ -0,0 +1 @@ +uid://bcga32vht26cr diff --git a/tools/check.sh b/tools/check.sh new file mode 100755 index 0000000..ab22af2 --- /dev/null +++ b/tools/check.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Parse-check every script in the project. ~5s, so run it after every edit -- +# it is the fastest signal that a change is syntactically and type-wise sound. +# +# Two stages, both necessary: +# 1. --import refreshes .godot/global_script_class_cache.cfg. Without it, a +# newly added `class_name` is invisible and every use of it reports +# "Identifier not declared", which looks like a real error but is not. +# 2. check.tscn loads every script so the engine prints real parse errors. +# Set SKIP_IMPORT=1 to skip stage 1 when you have not added a class_name. +set -uo pipefail +cd "$(dirname "$0")/.." +GODOT="${GODOT:-godot}" + +if [[ "${SKIP_IMPORT:-0}" != "1" ]]; then + "$GODOT" --headless --path . --import >/dev/null 2>&1 +fi + +out=$("$GODOT" --headless --path . res://tools/check.tscn 2>&1) + +if ! grep -q "CHECK_COMPLETE" <<<"$out"; then + echo "check.tscn did not finish -- engine output follows:" >&2 + echo "$out" >&2 + exit 2 +fi + +problems=$(grep -E "Parse Error|Failed to load script|does not inherit|SCRIPT ERROR" <<<"$out" || true) +if [[ -n "$problems" ]]; then + grep -E -A2 "Parse Error|Failed to load script|does not inherit|SCRIPT ERROR" <<<"$out" + echo + echo "FAIL: $(wc -l <<<"$problems") problem line(s)" >&2 + exit 1 +fi + +grep -o 'CHECK_COMPLETE scripts=[0-9]*' <<<"$out" | sed 's/CHECK_COMPLETE /clean, /' diff --git a/tools/check.tscn b/tools/check.tscn new file mode 100644 index 0000000..f80c940 --- /dev/null +++ b/tools/check.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/check_scripts.gd" id="1"] + +[node name="Check" type="Node"] +script = ExtResource("1") diff --git a/tools/check_scripts.gd b/tools/check_scripts.gd new file mode 100644 index 0000000..5d07084 --- /dev/null +++ b/tools/check_scripts.gd @@ -0,0 +1,48 @@ +extends Node +## Parse every GDScript in the project so the engine prints any parse or type +## error, then quit. +## +## Run as a scene (not with --script) because autoload identifiers such as `Net` +## are only registered when the engine boots a real main loop; a --script run +## reports false "Identifier not found" errors for every file that uses one. +## +## godot --headless --path . res://tools/check.tscn +## +## `tools/check.sh` wraps this and owns the exit code: a script that fails to +## parse can still come back from ResourceLoader as a non-null object, so the +## engine's own stderr is the reliable signal. + +const SKIP_DIRS := ["res://addons", "res://.godot"] + + +func _ready() -> void: + var scripts := _collect("res://") + scripts.sort() + for path in scripts: + ResourceLoader.load(path, "Script") + print("CHECK_COMPLETE scripts=%d" % scripts.size()) + get_tree().quit(0) + + +func _collect(dir_path: String) -> Array[String]: + var out: Array[String] = [] + for skip in SKIP_DIRS: + if dir_path.begins_with(skip): + return out + var dir := DirAccess.open(dir_path) + if dir == null: + return out + dir.list_dir_begin() + var entry := dir.get_next() + while entry != "": + if entry.begins_with("."): + entry = dir.get_next() + continue + var full := dir_path.path_join(entry) + if dir.current_is_dir(): + out.append_array(_collect(full)) + elif entry.ends_with(".gd"): + out.append(full) + entry = dir.get_next() + dir.list_dir_end() + return out diff --git a/tools/check_scripts.gd.uid b/tools/check_scripts.gd.uid new file mode 100644 index 0000000..d8024b5 --- /dev/null +++ b/tools/check_scripts.gd.uid @@ -0,0 +1 @@ +uid://gdilmx7e78su diff --git a/tools/client.sh b/tools/client.sh new file mode 100755 index 0000000..f46e567 --- /dev/null +++ b/tools/client.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Run a windowed client. With no arguments it opens the connect menu. +# tools/client.sh --join --name ada # skip the menu +# tools/client.sh --join --host 10.0.0.5 +# tools/client.sh --listen # listen server: host and play +set -euo pipefail +cd "$(dirname "$0")/.." +GODOT="${GODOT:-godot}" +exec "$GODOT" --path . -- "$@" diff --git a/tools/export_content.gd b/tools/export_content.gd new file mode 100644 index 0000000..39525bf --- /dev/null +++ b/tools/export_content.gd @@ -0,0 +1,36 @@ +extends SceneTree +## Writes .tres copies of everything in [Content] into res://resources/, so the +## numbers can be tuned in the editor inspector. +## +## godot --headless --path . --script tools/export_content.gd +## +## These files are an export, not the source of truth: [Content] is. Re-running +## this overwrites them. If you tune a value in the inspector and want to keep +## it, port it back into src/content/content.gd -- otherwise the next export +## silently reverts your change. + +const OUT_ENEMIES := "res://resources/enemies" +const OUT_BOSSES := "res://resources/bosses" + + +func _init() -> void: + DirAccess.make_dir_recursive_absolute(OUT_ENEMIES) + DirAccess.make_dir_recursive_absolute(OUT_BOSSES) + var written := 0 + for id in [Content.ENEMY_DRIFTER, Content.ENEMY_TURRET, Content.ENEMY_STALKER, + Content.ENEMY_DUMMY]: + written += _save(Content.enemy(id), "%s/%s.tres" % [OUT_ENEMIES, id]) + for id in [Content.BOSS_WARDEN]: + written += _save(Content.boss(id), "%s/%s.tres" % [OUT_BOSSES, id]) + print("exported %d resources" % written) + quit(0) + + +func _save(res: Resource, path: String) -> int: + # Sub-resources (the emitters) are inlined so one file is one whole enemy. + var err := ResourceSaver.save(res, path) + if err != OK: + printerr("failed to write %s (error %d)" % [path, err]) + return 0 + print(" ", path) + return 1 diff --git a/tools/export_content.gd.uid b/tools/export_content.gd.uid new file mode 100644 index 0000000..067bd96 --- /dev/null +++ b/tools/export_content.gd.uid @@ -0,0 +1 @@ +uid://b7gfvfowtnq2r diff --git a/tools/install-hooks.sh b/tools/install-hooks.sh new file mode 100755 index 0000000..e78e29e --- /dev/null +++ b/tools/install-hooks.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# One-time setup after cloning: point git at the hooks committed in .githooks/. +# git deliberately never runs hooks from a fresh clone on its own (that would +# be arbitrary code execution on `git clone`), so this is the one manual step. +set -euo pipefail +cd "$(dirname "$0")/.." +git config core.hooksPath .githooks +echo "hooks installed: pre-commit (check + test), pre-push (smoke)" diff --git a/tools/server.sh b/tools/server.sh new file mode 100755 index 0000000..400ad03 --- /dev/null +++ b/tools/server.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Run the dedicated server. Everything after -- is parsed by GameOpts. +# tools/server.sh # port 27015, headless +# tools/server.sh --port 27020 --verbose +set -euo pipefail +cd "$(dirname "$0")/.." +GODOT="${GODOT:-godot}" +exec "$GODOT" --headless --path . -- --server "$@" diff --git a/tools/setup_input_map.gd b/tools/setup_input_map.gd new file mode 100644 index 0000000..4ad1b77 --- /dev/null +++ b/tools/setup_input_map.gd @@ -0,0 +1,48 @@ +extends SceneTree +## Writes the project's input map into project.godot. +## +## Hand-editing the [input] section is error-prone -- the events are serialised +## engine objects with a long property list. Generating them through the real +## API means the file is always in the format the engine expects. +## +## godot --headless --path . --script tools/setup_input_map.gd +## +## Re-run after changing the bindings below. Physical keycodes are used so the +## bindings follow key position rather than layout. + + +func _init() -> void: + _action("move_up", [_key(KEY_W), _key(KEY_UP)]) + _action("move_down", [_key(KEY_S), _key(KEY_DOWN)]) + _action("move_left", [_key(KEY_A), _key(KEY_LEFT)]) + _action("move_right", [_key(KEY_D), _key(KEY_RIGHT)]) + _action("fire", [_mouse(MOUSE_BUTTON_LEFT), _key(KEY_SPACE)]) + _action("emergency_escape", [_key(KEY_F)]) + _action("interact", [_key(KEY_E)]) + var err := ProjectSettings.save() + print("input map written, err=%d" % err) + quit(0 if err == OK else 1) + + +func _action(name: String, events: Array) -> void: + ProjectSettings.set_setting("input/" + name, { + "deadzone": 0.2, + "events": events, + }) + + +## device -1 (DEVICE_ID_EMULATION) is what the editor writes and is the only +## value that matches input from a real device. A freshly constructed event +## defaults to 16, which silently matches nothing. +func _key(code: Key) -> InputEventKey: + var e := InputEventKey.new() + e.device = -1 + e.physical_keycode = code + return e + + +func _mouse(button: MouseButton) -> InputEventMouseButton: + var e := InputEventMouseButton.new() + e.device = -1 + e.button_index = button + return e diff --git a/tools/setup_input_map.gd.uid b/tools/setup_input_map.gd.uid new file mode 100644 index 0000000..ed342d5 --- /dev/null +++ b/tools/setup_input_map.gd.uid @@ -0,0 +1 @@ +uid://bd7ich1hfc40u diff --git a/tools/smoke.sh b/tools/smoke.sh new file mode 100755 index 0000000..9419db0 --- /dev/null +++ b/tools/smoke.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# End-to-end integration test: boot a dedicated server, connect two scripted +# bot clients over a real ENet socket, and assert that the whole loop happened -- +# handshake, lobby, portal into a dungeon, and the emergency escape back. +# +# This is the test that catches everything unit tests structurally cannot: RPC +# wiring, codec round-trips over the wire, instance transfers, and the client's +# reconciliation loop. Run it before calling any networking change done. +# +# tools/smoke.sh # ~35s +# KEEP=1 tools/smoke.sh # keep the logs and print where they are +set -uo pipefail +cd "$(dirname "$0")/.." +GODOT="${GODOT:-godot}" +PORT="${PORT:-27099}" +SERVER_TICKS="${SERVER_TICKS:-2000}" +CLIENT_TICKS="${CLIENT_TICKS:-1700}" +OUT="$(mktemp -d -t transcience-smoke-XXXXXX)" + +cleanup() { + kill %1 %2 %3 2>/dev/null + wait 2>/dev/null + if [[ "${KEEP:-0}" == "1" ]]; then + echo "logs kept in $OUT" + else + rm -rf "$OUT" + fi +} +trap cleanup EXIT + +echo "smoke: server on port $PORT, logs in $OUT" +"$GODOT" --headless --path . -- --server --port "$PORT" --autoquit "$SERVER_TICKS" \ + > "$OUT/server.log" 2>&1 & + +for _ in $(seq 1 60); do + grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null && break + sleep 0.25 +done +if ! grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null; then + echo "FAIL: server never became ready"; cat "$OUT/server.log"; exit 1 +fi + +for n in 1 2; do + "$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \ + --name "bot$n" --autoquit "$CLIENT_TICKS" > "$OUT/bot$n.log" 2>&1 & + sleep 0.4 +done + +wait %2 %3 2>/dev/null +wait %1 2>/dev/null + +fails=0 +check() { # check

-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

GB`7MGB`6tGB`72GB`6-GB`7IGB`8L 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 literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf.import b/addons/gut/fonts/LobsterTwo-Regular.ttf.import new file mode 100644 index 0000000..56cdadb --- /dev/null +++ b/addons/gut/fonts/LobsterTwo-Regular.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://5e8msj0ih2pv" +path="res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/LobsterTwo-Regular.ttf" +dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/OFL.txt b/addons/gut/fonts/OFL.txt new file mode 100644 index 0000000..3ed0152 --- /dev/null +++ b/addons/gut/fonts/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), +with Reserved Font Name Anonymous Pro. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/addons/gut/get_editor_interface.gd b/addons/gut/get_editor_interface.gd new file mode 100644 index 0000000..6ad312a --- /dev/null +++ b/addons/gut/get_editor_interface.gd @@ -0,0 +1,6 @@ +# This file is here so we can load it only when we are in the editor so that +# other places do not have to have "EditorInterface" in them, which causes a +# parser error when loaded outside of the editor. The things we have to do in +# order to test things is annoying. +func get_it(): + return EditorInterface \ No newline at end of file diff --git a/addons/gut/get_editor_interface.gd.uid b/addons/gut/get_editor_interface.gd.uid new file mode 100644 index 0000000..56aa1c1 --- /dev/null +++ b/addons/gut/get_editor_interface.gd.uid @@ -0,0 +1 @@ +uid://fgn2xo217kg1 diff --git a/addons/gut/godot_singletons.gd b/addons/gut/godot_singletons.gd new file mode 100644 index 0000000..2fab531 --- /dev/null +++ b/addons/gut/godot_singletons.gd @@ -0,0 +1,50 @@ + +# This file is auto-generated as part of the release process. GUT maintainers +# should not change this file manually. +static var class_ref = [ + AccessibilityServer, + AudioServer, + CameraServer, + ClassDB, + DisplayServer, + # excluded: EditorInterface, + Engine, + EngineDebugger, + GDExtensionManager, + # excluded: GDScriptLanguageProtocol, + Geometry2D, + Geometry3D, + IP, + Input, + InputMap, + JavaClassWrapper, + JavaScriptBridge, + Marshalls, + NativeMenu, + NavigationMeshGenerator, + NavigationServer2D, + NavigationServer2DManager, + NavigationServer3D, + NavigationServer3DManager, + OS, + Performance, + PhysicsServer2D, + PhysicsServer2DManager, + PhysicsServer3D, + PhysicsServer3DManager, + ProjectSettings, + RenderingServer, + ResourceLoader, + ResourceSaver, + ResourceUID, + TextServerManager, + ThemeDB, + Time, + TranslationServer, + WorkerThreadPool, + XRServer +] +static var names := [] +static func _static_init(): + for entry in class_ref: + names.append(entry.get_class()) diff --git a/addons/gut/godot_singletons.gd.uid b/addons/gut/godot_singletons.gd.uid new file mode 100644 index 0000000..89afbb4 --- /dev/null +++ b/addons/gut/godot_singletons.gd.uid @@ -0,0 +1 @@ +uid://cjkrqx36xfcbw diff --git a/addons/gut/gui/EditorRadioButton.tres b/addons/gut/gui/EditorRadioButton.tres new file mode 100644 index 0000000..fa04028 --- /dev/null +++ b/addons/gut/gui/EditorRadioButton.tres @@ -0,0 +1,13 @@ +[gd_resource type="Theme" format=3 uid="uid://dssgvu257o1si"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u716c"] +bg_color = Color(0.43137255, 0.8784314, 0.6156863, 0.5254902) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ht2pf"] +bg_color = Color(0, 0.44705883, 0.23921569, 1) + +[resource] +Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/styles/hover = SubResource("StyleBoxFlat_u716c") +Button/styles/pressed = SubResource("StyleBoxFlat_ht2pf") diff --git a/addons/gut/gui/GutBottomPanel.gd b/addons/gut/gui/GutBottomPanel.gd new file mode 100644 index 0000000..3918848 --- /dev/null +++ b/addons/gut/gui/GutBottomPanel.gd @@ -0,0 +1,563 @@ +@tool +extends Control + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') +var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd') +var AboutWindow = load("res://addons/gut/gui/about.tscn") + +var _interface = null; +var _is_running = false : + set(val): + _is_running = val + _disable_run_buttons(_is_running) + +var _gut_config = load('res://addons/gut/gut_config.gd').new() +var _gut_config_gui = null +var _gut_plugin = null +var _light_color = Color(0, 0, 0, .5) : + set(val): + _light_color = val + if(is_inside_tree()): + _ctrls.light.queue_redraw() +var _panel_button = null +var _user_prefs = null +var _shell_out_panel = null + + +var menu_manager = null : + set(val): + menu_manager = val + if(val != null): + _apply_shortcuts() + menu_manager.toggle_windowed.connect(_on_toggle_windowed) + menu_manager.about.connect(show_about) + menu_manager.run_all.connect(_run_all) + menu_manager.show_gut.connect(_on_show_gut) + + +@onready var _ctrls = { + about = %ExtraButtons/About, + light = %StatusIndicator, + output_button = %ExtraButtons/OutputBtn, + run_button = $layout/ControlBar/RunAll, + run_externally_dialog = $ShellOutOptions, + run_mode = %ExtraButtons/RunMode, + run_at_cursor = $layout/ControlBar/RunAtCursor, + run_results_button = %ExtraButtons/RunResultsBtn, + settings = $layout/RSplit/sc/Settings, + settings_button = %ExtraButtons/Settings, + shortcut_dialog = $ShortcutDialog, + shortcuts_button = %ExtraButtons/Shortcuts, + sub_panels_split = $layout/RSplit, + + results = { + bar = $layout/ControlBar2, + errors = %errors_value, + failing = %failing_value, + orphans = %orphans_value, + passing = %passing_value, + pending = %pending_value, + warnings = %warnings_value, + }, +} + +@onready var results_v_split = %VSplitResults +@onready var results_h_split = %HSplitResults +@onready var results_tree = %RunResults +@onready var results_text = %OutputText +@onready var make_floating_btn = %MakeFloating + + +func _ready(): + if(get_parent() is SubViewport): + return + + GutEditorGlobals.create_temp_directory() + + _user_prefs = GutEditorGlobals.user_prefs + _gut_config_gui = GutConfigGui.new(_ctrls.settings) + + _ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar)) + hide_settings(!_ctrls.settings_button.button_pressed) + + _gut_config.load_options(GutEditorGlobals.editor_run_gut_config_path) + _gut_config_gui.set_options(_gut_config.options) + + _ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons') + _ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons') + _ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree + _ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons') + make_floating_btn.icon = get_theme_icon("MakeFloating", 'EditorIcons') + make_floating_btn.text = '' + _ctrls.about.icon = get_theme_icon('Info', 'EditorIcons') + _ctrls.about.text = '' + _ctrls.run_mode.icon = get_theme_icon("ViewportSpeed", 'EditorIcons') + + results_tree.set_output_control(results_text) + + var check_import = load('res://addons/gut/images/HSplitContainer.svg') + if(check_import == null): + results_tree.add_centered_text("GUT got some new images that are not imported yet. Please restart Godot.") + print('GUT got some new images that are not imported yet. Please restart Godot.') + else: + results_tree.add_centered_text("Let's run some tests!") + + _ctrls.run_externally_dialog.load_from_file() + _apply_options_to_controls() + + results_vert_layout() + + +func _process(_delta): + if(_is_running): + if(_ctrls.run_externally_dialog.should_run_externally()): + if(!is_instance_valid(_shell_out_panel)): + _is_running = false + show_me() + elif(!_interface.is_playing_scene()): + _is_running = false + results_text.add_text("\ndone") + load_result_output() + show_me() + + +# --------------- +# Private +# --------------- +func _apply_options_to_controls(): + hide_settings(_user_prefs.hide_settings.value) + hide_result_tree(_user_prefs.hide_result_tree.value) + hide_output_text(_user_prefs.hide_output_text.value) + results_tree.set_show_orphans(!_gut_config.options.hide_orphans) + var shell_dialog_size = _user_prefs.run_externally_options_dialog_size.value + + if(shell_dialog_size != Vector2i(-1, -1)): + _ctrls.run_externally_dialog.size = Vector2i(shell_dialog_size) + + if(_user_prefs.shortcuts_dialog_size.value != Vector2i(-1, -1)): + _ctrls.shortcut_dialog.size = _user_prefs.shortcuts_dialog_size.value + + var mode_ind = 'Ed' + if(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_BLOCKING): + mode_ind = 'ExB' + elif(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_NON_BLOCKING): + mode_ind = 'ExN' + _ctrls.run_mode.text = mode_ind + + _ctrls.run_at_cursor.apply_gut_config(_gut_config) + + + +func _disable_run_buttons(should): + _ctrls.run_button.disabled = should + _ctrls.run_at_cursor.disabled = should + + +func _is_test_script(script): + var from = script.get_base_script() + while(from and from.resource_path != 'res://addons/gut/test.gd'): + from = from.get_base_script() + + return from != null + + +func _show_errors(errs): + results_text.clear() + var text = "Cannot run tests, you have a configuration error:\n" + for e in errs: + text += str('* ', e, "\n") + text += "Check your settings ----->" + results_text.add_text(text) + hide_output_text(false) + hide_settings(false) + + +func _save_user_prefs(): + _user_prefs.hide_settings.value = !_ctrls.settings_button.button_pressed + _user_prefs.hide_result_tree.value = !_ctrls.run_results_button.button_pressed + _user_prefs.hide_output_text.value = !_ctrls.output_button.button_pressed + _user_prefs.shortcuts_dialog_size.value = _ctrls.shortcut_dialog.size + + _user_prefs.run_externally.value = _ctrls.run_externally_dialog.run_mode != _ctrls.run_externally_dialog.RUN_MODE_EDITOR + _user_prefs.run_externally_options_dialog_size.value = _ctrls.run_externally_dialog.size + + _user_prefs.save_it() + + +func _save_config(): + _save_user_prefs() + + _gut_config.options = _gut_config_gui.get_options(_gut_config.options) + var w_result = _gut_config.write_options(GutEditorGlobals.editor_run_gut_config_path) + if(w_result != OK): + push_error(str('Could not write options to ', GutEditorGlobals.editor_run_gut_config_path, ': ', w_result)) + else: + _gut_config_gui.mark_saved() + + +func _run_externally(): + _shell_out_panel = GutUtils.RunExternallyScene.instantiate() + _shell_out_panel.bottom_panel = self + _shell_out_panel.blocking_mode = _ctrls.run_externally_dialog.run_mode + _shell_out_panel.additional_arguments = _ctrls.run_externally_dialog.get_additional_arguments_array() + + add_child(_shell_out_panel) + _shell_out_panel.run_tests() + + +func _run_tests(): + show_me() + if(_is_running): + push_error("GUT: Cannot run tests, tests are already running.") + return + + clear_results() + GutEditorGlobals.create_temp_directory() + _light_color = Color.BLUE + + var issues = _gut_config_gui.get_config_issues() + if(issues.size() > 0): + _show_errors(issues) + return + + write_file(GutEditorGlobals.editor_run_bbcode_results_path, 'Run in progress') + write_file(GutEditorGlobals.editor_run_json_results_path, '') + _save_config() + _apply_options_to_controls() + + results_text.clear() + results_tree.clear() + results_tree.add_centered_text('Running...') + + _is_running = true + results_text.add_text('Running...') + + if(_ctrls.run_externally_dialog.should_run_externally()): + _gut_plugin.make_bottom_panel_item_visible(self) + _run_externally() + else: + _interface.play_custom_scene('res://addons/gut/gui/run_from_editor.tscn') + + +func _apply_shortcuts(): + if(menu_manager != null): + menu_manager.apply_gut_shortcuts(_ctrls.shortcut_dialog) + + _ctrls.run_button.shortcut = \ + _ctrls.shortcut_dialog.scbtn_run_all.get_shortcut() + _ctrls.run_at_cursor.get_script_button().shortcut = \ + _ctrls.shortcut_dialog.scbtn_run_current_script.get_shortcut() + _ctrls.run_at_cursor.get_inner_button().shortcut = \ + _ctrls.shortcut_dialog.scbtn_run_current_inner.get_shortcut() + _ctrls.run_at_cursor.get_test_button().shortcut = \ + _ctrls.shortcut_dialog.scbtn_run_current_test.get_shortcut() + # Took this out because it seems to break using the shortcut when docked. + # Though it does allow the shortcut to work when windowed. Shortcuts + # are weird. + # make_floating_btn.shortcut = \ + # _ctrls.shortcut_dialog.scbtn_windowed.get_shortcut() + if(get_parent() is EditorDock): + get_parent().dock_shortcut = _ctrls.shortcut_dialog.scbtn_panel.get_shortcut() + + +func _run_all(): + _gut_config.options.selected = null + _gut_config.options.inner_class = null + _gut_config.options.unit_test_name = null + + _run_tests() + + +# --------------- +# Events +# --------------- +func _on_results_bar_draw(bar): + bar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2)) + + +func _on_Light_draw(): + var l = _ctrls.light + l.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color) + + +func _on_RunAll_pressed(): + _run_all() + + +func _on_Shortcuts_pressed(): + _ctrls.shortcut_dialog.popup_centered() + + +func _on_sortcut_dialog_confirmed() -> void: + _apply_shortcuts() + _ctrls.shortcut_dialog.save_shortcuts() + _save_user_prefs() + + +func _on_RunAtCursor_run_tests(what): + _gut_config.options.selected = what.script + _gut_config.options.inner_class = what.inner_class + _gut_config.options.unit_test_name = what.method + + _run_tests() + + +func _on_Settings_pressed(): + hide_settings(!_ctrls.settings_button.button_pressed) + _save_config() + + +func _on_OutputBtn_pressed(): + hide_output_text(!_ctrls.output_button.button_pressed) + _save_config() + + +func _on_RunResultsBtn_pressed(): + hide_result_tree(! _ctrls.run_results_button.button_pressed) + _save_config() + + +# Currently not used, but will be when I figure out how to put +# colors into the text results +func _on_UseColors_pressed(): + pass + + +func _on_shell_out_options_confirmed() -> void: + _ctrls.run_externally_dialog.save_to_file() + _save_user_prefs() + _apply_options_to_controls() + + +func _on_run_mode_pressed() -> void: + _ctrls.run_externally_dialog.popup_centered() + + +func _on_toggle_windowed(): + _gut_plugin.toggle_windowed() + + +func _on_to_window_pressed() -> void: + _gut_plugin.toggle_windowed() + + +func _on_show_gut() -> void: + show_hide() + + +func _on_about_pressed() -> void: + show_about() + + +func _on_horiz_layout_pressed() -> void: + results_horiz_layout() + + +func _on_vert_layout_pressed() -> void: + results_vert_layout() + + +func _on_resized() -> void: + # This wouldn't work in ready or defrred after ready. the split's y position + # was alwasy 0. This also adapts to Editor font size changes. + custom_minimum_size.y = _ctrls.sub_panels_split.position.y + + +# --------------- +# Public +# --------------- +func load_shortcuts(): + _ctrls.shortcut_dialog.load_shortcuts() + _apply_shortcuts() + + +func hide_result_tree(should): + results_tree.visible = !should + _ctrls.run_results_button.button_pressed = !should + + +func hide_settings(should): + var s_scroll = _ctrls.settings.get_parent() + s_scroll.visible = !should + + # collapse only collapses the first control, so we move + # settings around to be the collapsed one + if(should): + s_scroll.get_parent().move_child(s_scroll, 0) + else: + s_scroll.get_parent().move_child(s_scroll, 1) + + $layout/RSplit.collapsed = should + _ctrls.settings_button.button_pressed = !should + + +func hide_output_text(should): + results_text.visible = !should + _ctrls.output_button.button_pressed = !should + + +func clear_results(): + _light_color = Color(0, 0, 0, .5) + + _ctrls.results.passing.text = "0" + _ctrls.results.passing.get_parent().visible = false + + _ctrls.results.failing.text = "0" + _ctrls.results.failing.get_parent().visible = false + + _ctrls.results.pending.text = "0" + _ctrls.results.pending.get_parent().visible = false + + _ctrls.results.errors.text = "0" + _ctrls.results.errors.get_parent().visible = false + + _ctrls.results.warnings.text = "0" + _ctrls.results.warnings.get_parent().visible = false + + _ctrls.results.orphans.text = "0" + _ctrls.results.orphans.get_parent().visible = false + + +func load_result_json(): + var summary = get_file_as_text(GutEditorGlobals.editor_run_json_results_path) + var test_json_conv = JSON.new() + if (test_json_conv.parse(summary) != OK): + return + var results = test_json_conv.get_data() + + results_tree.load_json_results(results) + + var summary_json = results['test_scripts']['props'] + _ctrls.results.passing.text = str(int(summary_json.passing)) + _ctrls.results.passing.get_parent().visible = true + + _ctrls.results.failing.text = str(int(summary_json.failures)) + _ctrls.results.failing.get_parent().visible = true + + _ctrls.results.pending.text = str(int(summary_json.pending) + int(summary_json.risky)) + _ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0' + + _ctrls.results.errors.text = str(int(summary_json.errors)) + _ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0' + + _ctrls.results.warnings.text = str(int(summary_json.warnings)) + _ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0' + + _ctrls.results.orphans.text = str(int(summary_json.orphans)) + _ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans + + if(summary_json.tests == 0): + _light_color = Color(1, 0, 0, .75) + elif(summary_json.failures != 0): + _light_color = Color(1, 0, 0, .75) + elif(summary_json.pending != 0 or summary_json.risky != 0): + _light_color = Color(1, 1, 0, .75) + else: + _light_color = Color(0, 1, 0, .75) + + _ctrls.light.visible = true + + +func load_result_text(): + results_text.load_file(GutEditorGlobals.editor_run_bbcode_results_path) + + +func load_result_output(): + load_result_text() + load_result_json() + + +func set_interface(value): + _interface = value + results_tree.set_interface(_interface) + + +func set_plugin(value): + _gut_plugin = value + + +func set_panel_button(value): + _panel_button = value + + +func write_file(path, content): + var f = FileAccess.open(path, FileAccess.WRITE) + if(f != null): + f.store_string(content) + f = null; + + return FileAccess.get_open_error() + + +func get_file_as_text(path): + var to_return = '' + var f = FileAccess.open(path, FileAccess.READ) + if(f != null): + to_return = f.get_as_text() + f = null + return to_return + + +func get_text_output_control(): + return results_text + + +func add_output_text(text): + results_text.add_text(text) + + +func show_about(): + var about = AboutWindow.instantiate() + add_child(about) + about.popup_centered() + about.confirmed.connect(about.queue_free) + + +func show_me(): + get_parent().make_visible() + + +func show_hide(): + if(owner is Window): + if(owner.has_focus()): + var win_to_focus_on = EditorInterface.get_editor_main_screen().get_parent() + while(win_to_focus_on != null and win_to_focus_on is not Window): + win_to_focus_on = win_to_focus_on.get_parent() + if(win_to_focus_on != null): + win_to_focus_on.grab_focus() + else: + owner.grab_focus() + else: + pass + # We don't have to do anything when we are docked because the GUT + # bottom panel has the shortcut and it does the toggling all on its + # own. + + +func get_shortcut_dialog(): + return _ctrls.shortcut_dialog + + +func results_vert_layout(): + if(results_tree.get_parent() != results_v_split): + results_tree.reparent(results_v_split) + results_text.reparent(results_v_split) + results_v_split.visible = true + results_h_split.visible = false + + +func results_horiz_layout(): + if(results_tree.get_parent() != results_h_split): + results_tree.reparent(results_h_split) + results_text.reparent(results_h_split) + results_v_split.visible = false + results_h_split.visible = true + + +func show_layout_buttons(should): + %HorizLayout.visible = should + %VertLayout.visible = should + + +func get_panel_shortcut(): + _ctrls.shortcut_dialog.scbtn_panel.get_shortcut() diff --git a/addons/gut/gui/GutBottomPanel.gd.uid b/addons/gut/gui/GutBottomPanel.gd.uid new file mode 100644 index 0000000..0de51df --- /dev/null +++ b/addons/gut/gui/GutBottomPanel.gd.uid @@ -0,0 +1 @@ +uid://dtvnb0xatk0my diff --git a/addons/gut/gui/GutBottomPanel.tscn b/addons/gut/gui/GutBottomPanel.tscn new file mode 100644 index 0000000..15cef48 --- /dev/null +++ b/addons/gut/gui/GutBottomPanel.tscn @@ -0,0 +1,321 @@ +[gd_scene format=3 uid="uid://b3bostcslstem"] + +[ext_resource type="Script" uid="uid://dtvnb0xatk0my" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"] +[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"] +[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="4_xv2r3"] +[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"] +[ext_resource type="Texture2D" uid="uid://ljc2viafngwd" path="res://addons/gut/images/HSplitContainer.svg" id="5_qdqpf"] +[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"] +[ext_resource type="Texture2D" uid="uid://bhew20crsywxr" path="res://addons/gut/images/VSplitContainer.svg" id="6_ot46d"] +[ext_resource type="PackedScene" uid="uid://dj5ve0bq7xa5j" path="res://addons/gut/gui/ShortcutDialog.tscn" id="7_srqj5"] +[ext_resource type="PackedScene" uid="uid://ckv5eh8xyrwbk" path="res://addons/gut/gui/ShellOutOptions.tscn" id="7_xv2r3"] + +[sub_resource type="Shortcut" id="9"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_bskl7"] + +[node name="GutBottomPanel" type="Control" unique_id=1136630421] +clip_contents = true +custom_minimum_size = Vector2(250, 0) +layout_mode = 3 +anchor_left = -0.0025866 +anchor_top = -0.00176575 +anchor_right = 0.997413 +anchor_bottom = 0.998234 +offset_left = 2.64868 +offset_top = 1.05945 +offset_right = 2.64862 +offset_bottom = 1.05945 +script = ExtResource("1") + +[node name="layout" type="VBoxContainer" parent="." unique_id=790416054] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="ControlBar" type="HBoxContainer" parent="layout" unique_id=672788595] +layout_mode = 2 + +[node name="RunAll" type="Button" parent="layout/ControlBar" unique_id=678231332] +layout_mode = 2 +size_flags_vertical = 11 +shortcut = SubResource("9") +text = "Run All" +icon = ExtResource("4") + +[node name="Sep3" type="ColorRect" parent="layout/ControlBar" unique_id=1600176902] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="RunAtCursor" parent="layout/ControlBar" unique_id=1603612118 instance=ExtResource("3")] +custom_minimum_size = Vector2(488, 0) +layout_mode = 2 + +[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar" unique_id=269691286] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MakeFloating" type="Button" parent="layout/ControlBar" unique_id=1273743620] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Move the GUT panel to a window." +icon = ExtResource("4_xv2r3") +flat = true + +[node name="HorizLayout" type="Button" parent="layout/ControlBar" unique_id=679134345] +unique_name_in_owner = true +texture_filter = 1 +layout_mode = 2 +toggle_mode = true +button_pressed = true +button_group = SubResource("ButtonGroup_bskl7") +icon = ExtResource("5_qdqpf") +icon_alignment = 1 + +[node name="VertLayout" type="Button" parent="layout/ControlBar" unique_id=1762257478] +unique_name_in_owner = true +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bskl7") +icon = ExtResource("6_ot46d") + +[node name="ControlBar2" type="HBoxContainer" parent="layout" unique_id=920578685] +layout_mode = 2 + +[node name="Sep2" type="ColorRect" parent="layout/ControlBar2" unique_id=781273927] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 +color = Color(1, 1, 1, 0) + +[node name="StatusIndicator" type="Control" parent="layout/ControlBar2" unique_id=173224142] +unique_name_in_owner = true +custom_minimum_size = Vector2(30, 30) +layout_mode = 2 + +[node name="Passing" type="HBoxContainer" parent="layout/ControlBar2" unique_id=2110519605] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Passing" unique_id=164849935] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Passing" unique_id=1437040616] +layout_mode = 2 +text = "Pass" + +[node name="passing_value" type="Label" parent="layout/ControlBar2/Passing" unique_id=551548364] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="Failing" type="HBoxContainer" parent="layout/ControlBar2" unique_id=1824266070] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Failing" unique_id=1239759559] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Failing" unique_id=1956836855] +layout_mode = 2 +text = "Fail" + +[node name="failing_value" type="Label" parent="layout/ControlBar2/Failing" unique_id=636519064] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="Pending" type="HBoxContainer" parent="layout/ControlBar2" unique_id=1121510368] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Pending" unique_id=896038473] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Pending" unique_id=1642849369] +layout_mode = 2 +text = "Risky" + +[node name="pending_value" type="Label" parent="layout/ControlBar2/Pending" unique_id=162567404] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="Orphans" type="HBoxContainer" parent="layout/ControlBar2" unique_id=2040966734] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Orphans" unique_id=153606264] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Orphans" unique_id=1507291727] +layout_mode = 2 +text = "Orphans" + +[node name="orphans_value" type="Label" parent="layout/ControlBar2/Orphans" unique_id=235887326] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="Errors" type="HBoxContainer" parent="layout/ControlBar2" unique_id=1697483051] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Errors" unique_id=411797244] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Errors" unique_id=299959536] +layout_mode = 2 +text = "Errors" + +[node name="errors_value" type="Label" parent="layout/ControlBar2/Errors" unique_id=1340004517] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="Warnings" type="HBoxContainer" parent="layout/ControlBar2" unique_id=1097631959] +visible = false +layout_mode = 2 + +[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Warnings" unique_id=763793318] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="label" type="Label" parent="layout/ControlBar2/Warnings" unique_id=272711725] +layout_mode = 2 +text = "Warnings" + +[node name="warnings_value" type="Label" parent="layout/ControlBar2/Warnings" unique_id=868414665] +unique_name_in_owner = true +layout_mode = 2 +text = "---" + +[node name="CenterContainer" type="CenterContainer" parent="layout/ControlBar2" unique_id=1705687195] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ExtraButtons" type="HBoxContainer" parent="layout/ControlBar2" unique_id=1132817721] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Sep1" type="ColorRect" parent="layout/ControlBar2/ExtraButtons" unique_id=601143834] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="RunMode" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=1469737845] +layout_mode = 2 +tooltip_text = "Run Mode. Run tests through the editor or externally in a different process." +text = "ExN" +icon = ExtResource("4_xv2r3") + +[node name="Sep2" type="ColorRect" parent="layout/ControlBar2/ExtraButtons" unique_id=710471274] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="RunResultsBtn" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=385339391] +layout_mode = 2 +tooltip_text = "Show/Hide Result Tree" +toggle_mode = true +button_pressed = true +icon = ExtResource("4_xv2r3") + +[node name="OutputBtn" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=1461861149] +layout_mode = 2 +tooltip_text = "Show/Hide Text Output" +toggle_mode = true +button_pressed = true +icon = ExtResource("4_xv2r3") + +[node name="Settings" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=1063789901] +layout_mode = 2 +tooltip_text = "GUT Settings" +toggle_mode = true +button_pressed = true +icon = ExtResource("4_xv2r3") + +[node name="Sep3" type="ColorRect" parent="layout/ControlBar2/ExtraButtons" unique_id=650070116] +custom_minimum_size = Vector2(1, 2.08165e-12) +layout_mode = 2 + +[node name="Shortcuts" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=545241830] +layout_mode = 2 +size_flags_vertical = 11 +tooltip_text = "GUT Shortcuts" +icon = ExtResource("4_xv2r3") + +[node name="About" type="Button" parent="layout/ControlBar2/ExtraButtons" unique_id=1763800911] +layout_mode = 2 +tooltip_text = "About" +text = "A" + +[node name="RSplit" type="HSplitContainer" parent="layout" unique_id=153697410] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="CResults" type="VBoxContainer" parent="layout/RSplit" unique_id=124140650] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HSplitResults" type="HSplitContainer" parent="layout/RSplit/CResults" unique_id=241208440] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="RunResults" parent="layout/RSplit/CResults/HSplitResults" unique_id=1277406275 instance=ExtResource("5")] +unique_name_in_owner = true +custom_minimum_size = Vector2(255, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="OutputText" parent="layout/RSplit/CResults/HSplitResults" unique_id=1658148044 instance=ExtResource("6")] +unique_name_in_owner = true +layout_mode = 2 + +[node name="VSplitResults" type="VSplitContainer" parent="layout/RSplit/CResults" unique_id=394710781] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_vertical = 3 + +[node name="sc" type="ScrollContainer" parent="layout/RSplit" unique_id=978940325] +custom_minimum_size = Vector2(500, 2.08165e-12) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc" unique_id=1724913756] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="ShortcutDialog" parent="." unique_id=1224532527 instance=ExtResource("7_srqj5")] +size = Vector2i(1512, 1571) +visible = false + +[node name="ShellOutOptions" parent="." unique_id=1974075127 instance=ExtResource("7_xv2r3")] +size = Vector2i(1300, 1336) +visible = false + +[connection signal="resized" from="." to="." method="_on_resized"] +[connection signal="pressed" from="layout/ControlBar/RunAll" to="." method="_on_RunAll_pressed"] +[connection signal="run_tests" from="layout/ControlBar/RunAtCursor" to="." method="_on_RunAtCursor_run_tests"] +[connection signal="pressed" from="layout/ControlBar/MakeFloating" to="." method="_on_to_window_pressed"] +[connection signal="pressed" from="layout/ControlBar/HorizLayout" to="." method="_on_horiz_layout_pressed"] +[connection signal="pressed" from="layout/ControlBar/VertLayout" to="." method="_on_vert_layout_pressed"] +[connection signal="draw" from="layout/ControlBar2/StatusIndicator" to="." method="_on_Light_draw"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunMode" to="." method="_on_run_mode_pressed"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunResultsBtn" to="." method="_on_RunResultsBtn_pressed"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/OutputBtn" to="." method="_on_OutputBtn_pressed"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Settings" to="." method="_on_Settings_pressed"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Shortcuts" to="." method="_on_Shortcuts_pressed"] +[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/About" to="." method="_on_about_pressed"] +[connection signal="confirmed" from="ShortcutDialog" to="." method="_on_sortcut_dialog_confirmed"] +[connection signal="confirmed" from="ShellOutOptions" to="." method="_on_shell_out_options_confirmed"] diff --git a/addons/gut/gui/GutControl.gd b/addons/gut/gui/GutControl.gd new file mode 100644 index 0000000..ba63822 --- /dev/null +++ b/addons/gut/gui/GutControl.gd @@ -0,0 +1,326 @@ +@tool +extends Control + +const RUNNER_JSON_PATH = 'res://.gut_editor_config.json' + +var GutConfig = load('res://addons/gut/gut_config.gd') +var GutRunnerScene = load('res://addons/gut/gui/GutRunner.tscn') +var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd') + +var _config = GutConfig.new() +var _config_gui = null +var _gut_runner = null +var _tree_root : TreeItem = null + +var _script_icon = load('res://addons/gut/images/Script.svg') +var _folder_icon = load('res://addons/gut/images/Folder.svg') + +var _tree_scripts = {} +var _tree_directories = {} + +const TREE_SCRIPT = 'Script' +const TREE_DIR = 'Directory' + +@onready var _ctrls = { + run_tests_button = $VBox/Buttons/RunTests, + run_selected = $VBox/Buttons/RunSelected, + test_tree = $VBox/Tabs/Tests, + settings_vbox = $VBox/Tabs/SettingsScroll/Settings, + tabs = $VBox/Tabs, + bg = $Bg +} + +@export var bg_color : Color = Color(.36, .36, .36) : + get: return bg_color + set(val): + bg_color = val + if(is_inside_tree()): + $Bg.color = bg_color + + +func _ready(): + if Engine.is_editor_hint(): + return + + _gut_runner = GutRunnerScene.instantiate() + $Bg.color = bg_color + _ctrls.tabs.set_tab_title(0, 'Tests') + _ctrls.tabs.set_tab_title(1, 'Settings') + + _config_gui = GutConfigGui.new(_ctrls.settings_vbox) + + _ctrls.test_tree.hide_root = true + add_child(_gut_runner) + + # TODO This might not need to be called deferred after changing GutUtils to + # an all static class. + call_deferred('_post_ready') + + +func _draw(): + if Engine.is_editor_hint(): + return + + var gut = _gut_runner.get_gut() + if(!gut.is_running()): + var r = Rect2(Vector2(0, 0), get_rect().size) + draw_rect(r, Color.BLACK, false, 2) + + +func _post_ready(): + var gut = _gut_runner.get_gut() + gut.start_run.connect(_on_gut_run_started) + gut.end_run.connect(_on_gut_run_ended) + _refresh_tree_and_settings() + + +func _set_meta_for_script_tree_item(item, script, test=null): + var meta = { + type = TREE_SCRIPT, + script = script.path, + inner_class = script.inner_class_name, + test = '' + } + + if(test != null): + meta.test = test.name + + item.set_metadata(0, meta) + + +func _set_meta_for_directory_tree_item(item, path, temp_item): + var meta = { + type = TREE_DIR, + path = path, + temp_item = temp_item + } + item.set_metadata(0, meta) + + +func _get_script_tree_item(script, parent_item): + if(!_tree_scripts.has(script.path)): + var item = _ctrls.test_tree.create_item(parent_item) + item.set_text(0, script.path.get_file()) + item.set_icon(0, _script_icon) + _tree_scripts[script.path] = item + _set_meta_for_script_tree_item(item, script) + + return _tree_scripts[script.path] + + +func _get_directory_tree_item(path): + var parent = _tree_root + if(!_tree_directories.has(path)): + + var item : TreeItem = null + if(parent != _tree_root): + item = parent.create_child(0) + else: + item = parent.create_child() + + _tree_directories[path] = item + item.collapsed = false + item.set_text(0, path) + item.set_icon(0, _folder_icon) + item.set_icon_modulate(0, Color.ROYAL_BLUE) + # temp_item is used in calls with move_before since you must use + # move_before or move_after to reparent tree items. This ensures that + # there is an item on all directories. These are deleted later. + var temp_item = item.create_child() + temp_item.set_text(0, '') + + _set_meta_for_directory_tree_item(item, path, temp_item) + + return _tree_directories[path] + + +func _find_dir_item_to_move_before(path): + var max_matching_len = 0 + var best_parent = null + + # Go through all the directory items finding the one that has the longest + # path that contains our path. + for key in _tree_directories.keys(): + if(path != key and path.begins_with(key) and key.length() > max_matching_len): + max_matching_len = key.length() + best_parent = _tree_directories[key] + + var to_return = null + if(best_parent != null): + to_return = best_parent.get_metadata(0).temp_item + return to_return + + +func _reorder_dir_items(): + var the_keys = _tree_directories.keys() + the_keys.sort() + for key in _tree_directories.keys(): + var to_move = _tree_directories[key] + to_move.collapsed = false + var move_before = _find_dir_item_to_move_before(key) + if(move_before != null): + to_move.move_before(move_before) + var new_text = key.substr(move_before.get_parent().get_metadata(0).path.length()) + to_move.set_text(0, new_text) + + +func _remove_dir_temp_items(): + for key in _tree_directories.keys(): + var item = _tree_directories[key].get_metadata(0).temp_item + _tree_directories[key].remove_child(item) + + +func _add_dir_and_script_tree_items(): + var tree : Tree = _ctrls.test_tree + tree.clear() + _tree_root = _ctrls.test_tree.create_item() + + var scripts = _gut_runner.get_gut().get_test_collector().scripts + for script in scripts: + var dir_item = _get_directory_tree_item(script.path.get_base_dir()) + var item = _get_script_tree_item(script, dir_item) + + if(script.inner_class_name != ''): + var inner_item = tree.create_item(item) + inner_item.set_text(0, script.inner_class_name) + _set_meta_for_script_tree_item(inner_item, script) + item = inner_item + + for test in script.tests: + var test_item = tree.create_item(item) + test_item.set_text(0, test.name) + _set_meta_for_script_tree_item(test_item, script, test) + + +func _populate_tree(): + _add_dir_and_script_tree_items() + _tree_root.set_collapsed_recursive(true) + _tree_root.set_collapsed(false) + _reorder_dir_items() + _remove_dir_temp_items() + + +func _refresh_tree_and_settings(): + _config.apply_options(_gut_runner.get_gut()) + _gut_runner.set_gut_config(_config) + _populate_tree() + +# --------------------------- +# Events +# --------------------------- +func _on_gut_run_started(): + _ctrls.run_tests_button.disabled = true + _ctrls.run_selected.visible = false + _ctrls.tabs.visible = false + _ctrls.bg.visible = false + _ctrls.run_tests_button.text = 'Running' + queue_redraw() + + +func _on_gut_run_ended(): + _ctrls.run_tests_button.disabled = false + _ctrls.run_selected.visible = true + _ctrls.tabs.visible = true + _ctrls.bg.visible = true + _ctrls.run_tests_button.text = 'Run All' + queue_redraw() + + +func _on_run_tests_pressed(): + run_all() + + +func _on_run_selected_pressed(): + run_selected() + + +func _on_tests_item_activated(): + run_selected() + +# --------------------------- +# Public +# --------------------------- +func get_gut(): + return _gut_runner.get_gut() + + +func get_config(): + return _config + + +func run_all(): + _config.options.selected = '' + _config.options.inner_class_name = '' + _config.options.unit_test_name = '' + run_tests() + + +func run_tests(options = null): + if(options == null): + _config.options = _config_gui.get_options(_config.options) + else: + _config.options = options + + # We ar running from within the game, so we should not exit, ever. + _config.options.should_exit_on_success = false + _config.options.should_exit = false + + _gut_runner.get_gut().get_test_collector().clear() + _gut_runner.set_gut_config(_config) + _gut_runner.run_tests() + + +func run_selected(): + var sel_item = _ctrls.test_tree.get_selected() + if(sel_item == null): + return + + var options = _config_gui.get_options(_config.options) + var meta = sel_item.get_metadata(0) + if(meta.type == TREE_SCRIPT): + options.selected = meta.script.get_file() + options.inner_class_name = meta.inner_class + options.unit_test_name = meta.test + elif(meta.type == TREE_DIR): + options.dirs = [meta.path] + options.include_subdirectories = true + options.selected = '' + options.inner_class_name = '' + options.unit_test_name = '' + + run_tests(options) + + +func load_config_file(path): + _config.load_options(path) + _config.options.selected = '' + _config.options.inner_class_name = '' + _config.options.unit_test_name = '' + _config_gui.load_file(path) + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gui/GutControl.gd.uid b/addons/gut/gui/GutControl.gd.uid new file mode 100644 index 0000000..5776194 --- /dev/null +++ b/addons/gut/gui/GutControl.gd.uid @@ -0,0 +1 @@ +uid://cqlvpwidawld6 diff --git a/addons/gut/gui/GutControl.tscn b/addons/gut/gui/GutControl.tscn new file mode 100644 index 0000000..58301a2 --- /dev/null +++ b/addons/gut/gui/GutControl.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=2 format=3 uid="uid://4jb53yqktyfg"] + +[ext_resource type="Script" uid="uid://cqlvpwidawld6" path="res://addons/gut/gui/GutControl.gd" id="1_eprql"] + +[node name="GutControl" type="Control"] +layout_mode = 3 +anchors_preset = 0 +offset_right = 295.0 +offset_bottom = 419.0 +script = ExtResource("1_eprql") + +[node name="Bg" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.36, 0.36, 0.36, 1) + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Tabs" type="TabContainer" parent="VBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Tests" type="Tree" parent="VBox/Tabs"] +layout_mode = 2 +size_flags_vertical = 3 +hide_root = true + +[node name="SettingsScroll" type="ScrollContainer" parent="VBox/Tabs"] +visible = false +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Settings" type="VBoxContainer" parent="VBox/Tabs/SettingsScroll"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Buttons" type="HBoxContainer" parent="VBox"] +layout_mode = 2 + +[node name="RunTests" type="Button" parent="VBox/Buttons"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Run All" + +[node name="RunSelected" type="Button" parent="VBox/Buttons"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Run Selected" + +[connection signal="item_activated" from="VBox/Tabs/Tests" to="." method="_on_tests_item_activated"] +[connection signal="pressed" from="VBox/Buttons/RunTests" to="." method="_on_run_tests_pressed"] +[connection signal="pressed" from="VBox/Buttons/RunSelected" to="." method="_on_run_selected_pressed"] diff --git a/addons/gut/gui/GutLogo.tscn b/addons/gut/gui/GutLogo.tscn new file mode 100644 index 0000000..e73d259 --- /dev/null +++ b/addons/gut/gui/GutLogo.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=4 format=3 uid="uid://bjkn8mhx2fmt1"] + +[ext_resource type="Script" uid="uid://b8lvgepb64m8t" path="res://addons/gut/gui/gut_logo.gd" id="1_ba6lh"] +[ext_resource type="Texture2D" uid="uid://dyxbmyvpkkcvs" path="res://addons/gut/images/GutIconV2_base.png" id="2_ba6lh"] +[ext_resource type="Texture2D" uid="uid://dx0yxxn5q7doc" path="res://addons/gut/images/eyey.png" id="3_rc8fb"] + +[node name="Logo" type="Node2D"] +script = ExtResource("1_ba6lh") + +[node name="BaseLogo" type="Sprite2D" parent="."] +scale = Vector2(0.5, 0.5) +texture = ExtResource("2_ba6lh") + +[node name="LeftEye" type="Sprite2D" parent="BaseLogo"] +visible = false +position = Vector2(-238, 16) +texture = ExtResource("3_rc8fb") + +[node name="RightEye" type="Sprite2D" parent="BaseLogo"] +visible = false +position = Vector2(239, 16) +texture = ExtResource("3_rc8fb") + +[node name="ResetTimer" type="Timer" parent="."] +wait_time = 5.0 +one_shot = true + +[node name="FaceButton" type="Button" parent="."] +modulate = Color(1, 1, 1, 0) +offset_left = -141.0 +offset_top = -113.0 +offset_right = 140.0 +offset_bottom = 175.0 + +[connection signal="timeout" from="ResetTimer" to="." method="_on_reset_timer_timeout"] +[connection signal="pressed" from="FaceButton" to="." method="_on_face_button_pressed"] diff --git a/addons/gut/gui/GutRunner.gd b/addons/gut/gui/GutRunner.gd new file mode 100644 index 0000000..767d4c8 --- /dev/null +++ b/addons/gut/gui/GutRunner.gd @@ -0,0 +1,247 @@ +# ############################################################################## +# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a +# run of a test suite. +# +# This creates its own instance of gut.gd that it manages. You can set the +# gut.gd instance if you need to for testing. +# +# Set gut_config to an instance of a configured gut_config.gd instance prior to +# running tests. +# +# This will create a GUI and wire it up and apply gut_config.gd options. +# +# Running tests: Call run_tests +# ############################################################################## +extends Node2D + +const EXIT_OK = 0 +const EXIT_ERROR = 1 + +var Gut = load('res://addons/gut/gut.gd') +var ResultExporter = load('res://addons/gut/result_exporter.gd') +var GutConfig = load('res://addons/gut/gut_config.gd') + +var runner_json_path = null +var result_bbcode_path = null +var result_json_path = null + +var lgr = GutUtils.get_logger() +var gut_config = null + +var error_tracker = GutUtils.get_error_tracker() + +var _hid_gut = null; +# Lazy loaded gut instance. Settable for testing purposes. +var gut = _hid_gut : + get: + if(_hid_gut == null): + _hid_gut = Gut.new(lgr) + _hid_gut.error_tracker = error_tracker + return _hid_gut + set(val): + _hid_gut = val + +var _wrote_results = false +var _ran_from_editor = false + +@onready var _gut_layer = $GutLayer +@onready var _gui = $GutLayer/GutScene + + +func _ready(): + GutUtils.WarningsManager.apply_warnings_dictionary( + GutUtils.warnings_at_start) + + +func _exit_tree(): + if(!_wrote_results and _ran_from_editor): + _write_results_for_gut_panel() + + +func _setup_gui(show_gui): + if(show_gui): + _gui.gut = gut + var printer = gut.logger.get_printer('gui') + printer.set_textbox(_gui.get_textbox()) + else: + gut.logger.disable_printer('gui', true) + _gui.visible = false + + var opts = gut_config.options + _gui.set_font_size(opts.font_size) + _gui.set_font(opts.font_name) + if(opts.font_color != null and opts.font_color.is_valid_html_color()): + _gui.set_default_font_color(Color(opts.font_color)) + if(opts.background_color != null and opts.background_color.is_valid_html_color()): + _gui.set_background_color(Color(opts.background_color)) + + _gui.set_opacity(min(1.0, float(opts.opacity) / 100)) + _gui.use_compact_mode(opts.compact_mode) + + +func _write_results_for_gut_panel(): + var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode() + var f = FileAccess.open(result_bbcode_path, FileAccess.WRITE) + if(f != null): + f.store_string(content) + f = null # closes file + else: + push_error('Could not save bbcode, result = ', FileAccess.get_open_error()) + + var exporter = ResultExporter.new() + # TODO this should be checked and _wrote_results should maybe not be set, or + # maybe we do not care. Whichever, it should be clear. + var _f_result = exporter.write_json_file(gut, result_json_path) + _wrote_results = true + + +func _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK): + var quitting_time = should_exit or \ + (should_exit_on_success and gut.get_fail_count() == 0) or \ + GutUtils.is_headless() + + if(!quitting_time): + if(should_exit_on_success): + lgr.log("There are failing tests, exit manually.") + _gui.use_compact_mode(false) + return + + # For some reason, tests fail asserting that quit was called with 0 if we + # do not do this, but everything is defaulted so I don't know why it gets + # null. + var exit_code = GutUtils.nvl(override_exit_code, EXIT_OK) + + if(gut.get_fail_count() > 0): + exit_code = EXIT_ERROR + + # Overwrite the exit code with the post_script's exit code if it is set + var post_hook_inst = gut.get_post_run_script_instance() + if(post_hook_inst != null and post_hook_inst.get_exit_code() != null): + exit_code = post_hook_inst.get_exit_code() + + quit(exit_code) + + +func _end_run(override_exit_code=EXIT_OK): + if(_ran_from_editor): + _write_results_for_gut_panel() + + GutErrorTracker.deregister_logger(error_tracker) + + _handle_quit(gut_config.options.should_exit, + gut_config.options.should_exit_on_success, + override_exit_code) + + +# ------------- +# Events +# ------------- +func _on_tests_finished(): + _end_run() + + +# ------------- +# Public +# ------------- +# For internal use only, but still public. Consider it "protected" and you +# don't have my permission to call this, unless "you" is "me". +func run_from_editor(): + _ran_from_editor = true + var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path) + result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path) + result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path) + + if(gut_config == null): + gut_config = GutConfig.new() + gut_config.load_options(runner_json_path) + + call_deferred('run_tests') + + +func run_tests(show_gui=true): + _setup_gui(show_gui) + + if(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0): + var err_text = "You do not have any directories configured, so GUT " + \ + "doesn't know where to find the tests. Tell GUT where to find the " + \ + "tests and GUT shall run the tests." + lgr.error(err_text) + push_error(err_text) + _end_run(EXIT_ERROR) + return + + var install_check_text = GutUtils.make_install_check_text() + if(install_check_text != GutUtils.INSTALL_OK_TEXT): + print("\n\n", GutUtils.version_numbers.get_version_text()) + lgr.error(install_check_text) + push_error(install_check_text) + _end_run(EXIT_ERROR) + return + + gut.add_children_to = self + if(gut.get_parent() == null): + if(gut_config.options.gut_on_top): + _gut_layer.add_child(gut) + else: + add_child(gut) + + if(!gut.end_run.is_connected(_on_tests_finished)): + gut.end_run.connect(_on_tests_finished) + + gut_config.apply_options(gut) + if GutUtils.is_headless(): + gut._ignore_pause_before_teardown = true + + var run_rest_of_scripts = gut_config.options.unit_test_name == '' + GutErrorTracker.register_logger(error_tracker) + gut.test_scripts(run_rest_of_scripts) + + +func set_gut_config(which): + gut_config = which + + +# for backwards compatibility +func get_gut(): + return gut + + +func quit(exit_code): + # Sometimes quitting takes a few seconds. This gives some indicator + # of what is going on. + _gui.set_title("Exiting") + + await get_tree().process_frame + + lgr.info(str('Exiting with code ', exit_code)) + get_tree().quit(exit_code) + + + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gui/GutRunner.gd.uid b/addons/gut/gui/GutRunner.gd.uid new file mode 100644 index 0000000..b5971e8 --- /dev/null +++ b/addons/gut/gui/GutRunner.gd.uid @@ -0,0 +1 @@ +uid://eg8k46gd42a4 diff --git a/addons/gut/gui/GutRunner.tscn b/addons/gut/gui/GutRunner.tscn new file mode 100644 index 0000000..a4e966f --- /dev/null +++ b/addons/gut/gui/GutRunner.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=3 uid="uid://bqy3ikt6vu4b5"] + +[ext_resource type="Script" uid="uid://eg8k46gd42a4" path="res://addons/gut/gui/GutRunner.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://m28heqtswbuq" path="res://addons/gut/GutScene.tscn" id="2_6ruxb"] + +[node name="GutRunner" type="Node2D"] +script = ExtResource("1") + +[node name="GutLayer" type="CanvasLayer" parent="."] +layer = 128 + +[node name="GutScene" parent="GutLayer" instance=ExtResource("2_6ruxb")] diff --git a/addons/gut/gui/GutSceneTheme.tres b/addons/gut/gui/GutSceneTheme.tres new file mode 100644 index 0000000..157320a --- /dev/null +++ b/addons/gut/gui/GutSceneTheme.tres @@ -0,0 +1,7 @@ +[gd_resource type="Theme" load_steps=2 format=3 uid="uid://cstkhwkpajvqu"] + +[ext_resource type="FontFile" uid="uid://c6c7gnx36opr0" path="res://addons/gut/fonts/AnonymousPro-Regular.ttf" id="1_df57p"] + +[resource] +default_font = ExtResource("1_df57p") +Label/fonts/font = ExtResource("1_df57p") diff --git a/addons/gut/gui/MinGui.tscn b/addons/gut/gui/MinGui.tscn new file mode 100644 index 0000000..f347e5c --- /dev/null +++ b/addons/gut/gui/MinGui.tscn @@ -0,0 +1,161 @@ +[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"] + +[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"] +[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"] +[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"] +[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"] + +[node name="Min" type="Panel"] +clip_contents = true +custom_minimum_size = Vector2(280, 145) +offset_right = 280.0 +offset_bottom = 145.0 +theme = ExtResource("1_farmq") +script = ExtResource("2_eokrf") + +[node name="MainBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_layout_mode = 1 + +[node name="TitleBar" type="Panel" parent="MainBox"] +custom_minimum_size = Vector2(0, 25) +layout_mode = 2 + +[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 2.0 +offset_bottom = 3.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_layout_mode = 1 + +[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +text = "Title" + +[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +text = "0.000s" + +[node name="Body" type="HBoxContainer" parent="MainBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"] +layout_mode = 2 +text = "T:" + +[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 3 +value = 25.0 + +[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"] +layout_mode = 2 +text = "S:" + +[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 3 +value = 75.0 + +[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"] +clip_contents = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("2_a2e2l") +theme_override_font_sizes/font_size = 14 +text = "res://test/integration/whatever" +clip_text = true +text_overrun_behavior = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"] +clip_contents = true +layout_mode = 2 + +[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_fonts/font = ExtResource("2_a2e2l") +theme_override_font_sizes/font_size = 14 +text = "test_this_thing.gd" +text_overrun_behavior = 3 + +[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"] +layout_mode = 2 + +[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")] +layout_mode = 2 +orientation = 0 +resize_control = NodePath("../../../../..") +vertical_resize = false + +[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"] +layout_mode = 2 +text = "Expand" + +[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"] +layout_mode = 2 +text = "Continue +" + +[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")] +layout_mode = 2 +resize_control = NodePath("../../../../..") +vertical_resize = false + +[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="CenterContainer" type="CenterContainer" parent="MainBox"] +custom_minimum_size = Vector2(2.08165e-12, 2) +layout_mode = 2 diff --git a/addons/gut/gui/NormalGui.tscn b/addons/gut/gui/NormalGui.tscn new file mode 100644 index 0000000..e729d0d --- /dev/null +++ b/addons/gut/gui/NormalGui.tscn @@ -0,0 +1,215 @@ +[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"] + +[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"] +[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"] +[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"] +[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"] + +[node name="Large" type="Panel"] +custom_minimum_size = Vector2(500, 150) +offset_right = 632.0 +offset_bottom = 260.0 +theme = ExtResource("1_5hlsm") +script = ExtResource("2_fue6q") + +[node name="MainBox" type="VBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_layout_mode = 1 + +[node name="TitleBar" type="Panel" parent="MainBox"] +custom_minimum_size = Vector2(0, 25) +layout_mode = 2 + +[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 2.0 +offset_bottom = 3.0 +grow_horizontal = 2 +grow_vertical = 2 +metadata/_edit_layout_mode = 1 + +[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +text = "Title" + +[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"] +custom_minimum_size = Vector2(90, 0) +layout_mode = 2 +text = "999.999s" + +[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +color = Color(0.0745098, 0.0705882, 0.0784314, 1) +metadata/_edit_layout_mode = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +bbcode_enabled = true +scroll_following = true +autowrap_mode = 0 +selection_enabled = true + +[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"] +custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) +layout_mode = 2 + +[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"] +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Tests" + +[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +value = 25.0 + +[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"] +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Scripts" + +[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +value = 75.0 + +[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"] +layout_mode = 2 +size_flags_vertical = 6 +theme_override_fonts/font = ExtResource("2_u5uc1") +theme_override_font_sizes/font_size = 14 +text = "res://test/integration/whatever" +text_overrun_behavior = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_fonts/font = ExtResource("2_u5uc1") +theme_override_font_sizes/font_size = 14 +text = "test_this_thing.gd" +text_overrun_behavior = 3 + +[node name="Buttons" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"] +layout_mode = 2 + +[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"] +layout_mode = 2 +size_flags_vertical = 4 +text = "Continue +" + +[node name="WordWrap" type="CheckButton" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"] +layout_mode = 2 +text = "Word Wrap" + +[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"] +custom_minimum_size = Vector2(5, 0) +layout_mode = 2 + +[node name="BottomPad" type="CenterContainer" parent="MainBox"] +custom_minimum_size = Vector2(0, 5) +layout_mode = 2 + +[node name="Footer" type="HBoxContainer" parent="MainBox"] +layout_mode = 2 + +[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"] +custom_minimum_size = Vector2(2, 2.08165e-12) +layout_mode = 2 + +[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")] +custom_minimum_size = Vector2(25, 25) +layout_mode = 2 +orientation = 0 +resize_control = NodePath("../../..") + +[node name="SwitchModes" type="Button" parent="MainBox/Footer"] +layout_mode = 2 +text = "Compact +" + +[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")] +custom_minimum_size = Vector2(25, 25) +layout_mode = 2 +resize_control = NodePath("../../..") + +[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"] +custom_minimum_size = Vector2(2, 2.08165e-12) +layout_mode = 2 + +[node name="BottomPad2" type="CenterContainer" parent="MainBox"] +custom_minimum_size = Vector2(2.08165e-12, 2) +layout_mode = 2 diff --git a/addons/gut/gui/OutputText.gd b/addons/gut/gui/OutputText.gd new file mode 100644 index 0000000..fddd999 --- /dev/null +++ b/addons/gut/gui/OutputText.gd @@ -0,0 +1,352 @@ +@tool +extends VBoxContainer + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') +var PanelControls = load('res://addons/gut/gui/panel_controls.gd') + +# ############################################################################## +# Keeps search results from the TextEdit +# ############################################################################## +class TextEditSearcher: + var te : TextEdit + var _last_term = '' + var _last_pos = Vector2(-1, -1) + var _ignore_caret_change = false + + func set_text_edit(which): + te = which + te.caret_changed.connect(_on_caret_changed) + + + func _on_caret_changed(): + if(_ignore_caret_change): + _ignore_caret_change = false + else: + _last_pos = _get_caret(); + + + func _get_caret(): + return Vector2(te.get_caret_column(), te.get_caret_line()) + + + func _set_caret_and_sel(pos, len): + te.set_caret_line(pos.y) + te.set_caret_column(pos.x) + if(len > 0): + te.select(pos.y, pos.x, pos.y, pos.x + len) + + + func _find(term, search_flags): + var pos = _get_caret() + if(term == _last_term): + if(search_flags == 0): + pos = _last_pos + pos.x += 1 + else: + pos = _last_pos + pos.x -= 1 + + var result = te.search(term, search_flags, pos.y, pos.x) +# print('searching from ', pos, ' for "', term, '" = ', result) + if(result.y != -1): + _ignore_caret_change = true + _set_caret_and_sel(result, term.length()) + _last_pos = result + + _last_term = term + + func find_next(term): + _find(term, 0) + + func find_prev(term): + _find(term, te.SEARCH_BACKWARDS) + + +# ############################################################################## +# Start OutputText control code +# ############################################################################## +@onready var _ctrls = { + output = $Output, + settings_bar = $Settings, + use_colors = $Settings/UseColors, + word_wrap = $Settings/WordWrap, + + copy_button = $Toolbar/CopyButton, + clear_button = $Toolbar/ClearButton, + show_search = $Toolbar/ShowSearch, + caret_position = $Toolbar/LblPosition, + + search_bar = { + bar = $Search, + search_term = $Search/SearchTerm, + } +} + +var _sr = TextEditSearcher.new() +var _highlighter : CodeHighlighter +var _font_name = null +var _user_prefs = GutEditorGlobals.user_prefs +var _font_name_pctrl = null +var _font_size_pctrl = null + +var keywords = [ + ['Failed', Color.RED], + ['Passed', Color.GREEN], + ['Pending', Color.YELLOW], + ['Risky', Color.YELLOW], + ['Orphans', Color.YELLOW], + ['WARNING', Color.YELLOW], + ['ERROR', Color.RED], + ['ExpectedError', Color.LIGHT_BLUE], +] + + +# Automatically used when running the OutputText scene from the editor. Changes +# to this method only affect test-running the control through the editor. +func _test_running_setup(): + _ctrls.use_colors.text = 'use colors' + _ctrls.show_search.text = 'search' + _ctrls.word_wrap.text = 'ww' + + set_all_fonts("CourierPrime") + set_font_size(30) + + _ctrls.output.queue_redraw() + load_file('user://.gut_editor.bbcode') + await get_tree().process_frame + + show_search(true) + _ctrls.output.set_caret_line(0) + _ctrls.output.scroll_vertical = 0 + _ctrls.output.caret_changed.connect(_on_caret_changed) + + +func _ready(): + if(get_parent() is SubViewport): + return + + _sr.set_text_edit(_ctrls.output) + _ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons') + _ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons') + _ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons') + + _setup_colors() + _ctrls.use_colors.button_pressed = true + _use_highlighting(true) + + if(get_parent() == get_tree().root): + _test_running_setup() + + _ctrls.settings_bar.visible = false + _add_other_ctrls() + + +func _add_other_ctrls(): + var fname = GutUtils.gut_fonts.DEFAULT_CUSTOM_FONT_NAME + if(_user_prefs != null): + fname = _user_prefs.output_font_name.value + _font_name_pctrl = PanelControls.GpcSelect.new('Font', fname, GutUtils.avail_fonts, + "The font, you know, for the text below. Change it, see what it does.") + _font_name_pctrl.changed.connect(_on_font_name_changed) + _font_name_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN + _ctrls.settings_bar.add_child(_font_name_pctrl) + set_all_fonts(fname) + + var fsize = 30 + if(_user_prefs != null): + fsize = _user_prefs.output_font_size.value + _font_size_pctrl = PanelControls.GpcNumber.new('Font Size', fsize , 5, 100, + "The size of 'The Font'.") + _font_size_pctrl.changed.connect(_on_font_size_changed) + _font_size_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN + _ctrls.settings_bar.add_child(_font_size_pctrl) + set_font_size(fsize) + + +# ------------------ +# Private +# ------------------ + +# Call this after changes in colors and the like to get them to apply. reloads +# the text of the output control. +func _refresh_output(): + var orig_pos = _ctrls.output.scroll_vertical + var text = _ctrls.output.text + + _ctrls.output.text = text + _ctrls.output.scroll_vertical = orig_pos + + +func _create_highlighter(default_color=Color(1, 1, 1, 1)): + var to_return = CodeHighlighter.new() + + to_return.function_color = default_color + to_return.number_color = default_color + to_return.symbol_color = default_color + to_return.member_variable_color = default_color + + for keyword in keywords: + to_return.add_keyword_color(keyword[0], keyword[1]) + + return to_return + + +func _setup_colors(): + _ctrls.output.clear() + _highlighter = _create_highlighter() + _ctrls.output.queue_redraw() + + + +func _use_highlighting(should): + if(should): + _ctrls.output.syntax_highlighter = _highlighter + else: + _ctrls.output.syntax_highlighter = null + _refresh_output() + +# ------------------ +# Events +# ------------------ +func _on_caret_changed(): + var txt = str("line:",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column()) + _ctrls.caret_position.text = str(txt) + +func _on_font_size_changed(): + set_font_size(_font_size_pctrl.value) + if(_user_prefs != null): + _user_prefs.output_font_size.value = _font_size_pctrl.value + _user_prefs.output_font_size.save_it() + +func _on_font_name_changed(): + set_all_fonts(_font_name_pctrl.text) + if(_user_prefs != null): + _user_prefs.output_font_name.value = _font_name_pctrl.text + _user_prefs.output_font_name.save_it() + +func _on_CopyButton_pressed(): + copy_to_clipboard() + +func _on_UseColors_pressed(): + _use_highlighting(_ctrls.use_colors.button_pressed) + +func _on_ClearButton_pressed(): + clear() + +func _on_ShowSearch_pressed(): + show_search(_ctrls.show_search.button_pressed) + +func _on_SearchTerm_focus_entered(): + _ctrls.search_bar.search_term.call_deferred('select_all') + +func _on_SearchNext_pressed(): + _sr.find_next(_ctrls.search_bar.search_term.text) + +func _on_SearchPrev_pressed(): + _sr.find_prev(_ctrls.search_bar.search_term.text) + +func _on_SearchTerm_text_changed(new_text): + if(new_text == ''): + _ctrls.output.deselect() + else: + _sr.find_next(new_text) + +func _on_SearchTerm_text_entered(new_text): + if(Input.is_physical_key_pressed(KEY_SHIFT)): + _sr.find_prev(new_text) + else: + _sr.find_next(new_text) + +func _on_SearchTerm_gui_input(event): + if(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE): + show_search(false) + +func _on_WordWrap_pressed(): + if(_ctrls.word_wrap.button_pressed): + _ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY + else: + _ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE + + _ctrls.output.queue_redraw() + +func _on_settings_pressed(): + _ctrls.settings_bar.visible = $Toolbar/ShowSettings.button_pressed + +# ------------------ +# Public +# ------------------ +func show_search(should): + _ctrls.search_bar.bar.visible = should + if(should): + _ctrls.search_bar.search_term.grab_focus() + _ctrls.search_bar.search_term.select_all() + _ctrls.show_search.button_pressed = should + + +func search(text, start_pos, highlight=true): + return _sr.find_next(text) + + +func copy_to_clipboard(): + var selected = _ctrls.output.get_selected_text() + if(selected != ''): + DisplayServer.clipboard_set(selected) + else: + DisplayServer.clipboard_set(_ctrls.output.text) + + +func clear(): + _ctrls.output.text = '' + + +func _set_font(custom_name, theme_font_name): + var font = GutUtils.gut_fonts.get_font_for_theme_font_name(theme_font_name, custom_name) + _ctrls.output.add_theme_font_override(theme_font_name, font) + + +func set_all_fonts(base_name): + _font_name = GutUtils.nvl(base_name, 'Default') + + _set_font(base_name, 'font') + _set_font(base_name, 'normal_font') + _set_font(base_name, 'bold_font') + _set_font(base_name, 'italics_font') + _set_font(base_name, 'bold_italics_font') + + +func set_font_size(new_size): + _ctrls.output.set("theme_override_font_sizes/font_size", new_size) + + +func set_use_colors(value): + pass + + +func get_use_colors(): + return false; + + +func get_rich_text_edit(): + return _ctrls.output + + +func load_file(path): + var f = FileAccess.open(path, FileAccess.READ) + if(f == null): + return + + var t = f.get_as_text() + f = null # closes file + _ctrls.output.text = t + _ctrls.output.scroll_vertical = _ctrls.output.get_line_count() + _ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count()) + + +func add_text(text): + if(is_inside_tree()): + _ctrls.output.text += text + + +func scroll_to_line(line): + _ctrls.output.scroll_vertical = line + _ctrls.output.set_caret_line(line) diff --git a/addons/gut/gui/OutputText.gd.uid b/addons/gut/gui/OutputText.gd.uid new file mode 100644 index 0000000..7538795 --- /dev/null +++ b/addons/gut/gui/OutputText.gd.uid @@ -0,0 +1 @@ +uid://cax5phqs8acmu diff --git a/addons/gut/gui/OutputText.tscn b/addons/gut/gui/OutputText.tscn new file mode 100644 index 0000000..b666c15 --- /dev/null +++ b/addons/gut/gui/OutputText.tscn @@ -0,0 +1,121 @@ +[gd_scene load_steps=5 format=3 uid="uid://bqmo4dj64c7yl"] + +[ext_resource type="Script" uid="uid://cax5phqs8acmu" path="res://addons/gut/gui/OutputText.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="2_b4xqv"] + +[sub_resource type="DPITexture" id="DPITexture_lygvu"] +_source = " +" + +[sub_resource type="CodeHighlighter" id="CodeHighlighter_8ynmy"] +number_color = Color(1, 1, 1, 1) +symbol_color = Color(1, 1, 1, 1) +function_color = Color(1, 1, 1, 1) +member_variable_color = Color(1, 1, 1, 1) +keyword_colors = { +"ERROR": Color(1, 0, 0, 1), +"ExpectedError": Color(0.6784314, 0.84705883, 0.9019608, 1), +"Failed": Color(1, 0, 0, 1), +"Orphans": Color(1, 1, 0, 1), +"Passed": Color(0, 1, 0, 1), +"Pending": Color(1, 1, 0, 1), +"Risky": Color(1, 1, 0, 1), +"WARNING": Color(1, 1, 0, 1) +} + +[node name="OutputText" type="VBoxContainer"] +offset_right = 862.0 +offset_bottom = 523.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="Toolbar" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShowSearch" type="Button" parent="Toolbar"] +layout_mode = 2 +tooltip_text = "Search" +toggle_mode = true +icon = ExtResource("2_b4xqv") + +[node name="ShowSettings" type="Button" parent="Toolbar"] +layout_mode = 2 +tooltip_text = "Settings" +toggle_mode = true +text = "..." + +[node name="CenterContainer" type="CenterContainer" parent="Toolbar"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="LblPosition" type="Label" parent="Toolbar"] +layout_mode = 2 + +[node name="CopyButton" type="Button" parent="Toolbar"] +layout_mode = 2 +text = " Copy " + +[node name="ClearButton" type="Button" parent="Toolbar"] +layout_mode = 2 +text = " Clear " + +[node name="Settings" type="HBoxContainer" parent="."] +visible = false +layout_mode = 2 + +[node name="WordWrap" type="Button" parent="Settings"] +layout_mode = 2 +tooltip_text = "Word Wrap" +toggle_mode = true +icon = SubResource("DPITexture_lygvu") + +[node name="UseColors" type="Button" parent="Settings"] +layout_mode = 2 +tooltip_text = "Colorized Text" +toggle_mode = true +button_pressed = true +icon = SubResource("DPITexture_lygvu") + +[node name="Output" type="TextEdit" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_font_sizes/font_size = 30 +deselect_on_focus_loss_enabled = false +virtual_keyboard_enabled = false +middle_mouse_paste_enabled = false +scroll_smooth = true +syntax_highlighter = SubResource("CodeHighlighter_8ynmy") +highlight_all_occurrences = true +highlight_current_line = true + +[node name="Search" type="HBoxContainer" parent="."] +visible = false +layout_mode = 2 + +[node name="SearchTerm" type="LineEdit" parent="Search"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SearchNext" type="Button" parent="Search"] +layout_mode = 2 +text = "Next" + +[node name="SearchPrev" type="Button" parent="Search"] +layout_mode = 2 +text = "Prev" + +[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"] +[connection signal="pressed" from="Toolbar/ShowSettings" to="." method="_on_settings_pressed"] +[connection signal="pressed" from="Toolbar/CopyButton" to="." method="_on_CopyButton_pressed"] +[connection signal="pressed" from="Toolbar/ClearButton" to="." method="_on_ClearButton_pressed"] +[connection signal="pressed" from="Settings/WordWrap" to="." method="_on_WordWrap_pressed"] +[connection signal="pressed" from="Settings/UseColors" to="." method="_on_UseColors_pressed"] +[connection signal="focus_entered" from="Search/SearchTerm" to="." method="_on_SearchTerm_focus_entered"] +[connection signal="gui_input" from="Search/SearchTerm" to="." method="_on_SearchTerm_gui_input"] +[connection signal="text_changed" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_changed"] +[connection signal="text_submitted" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_entered"] +[connection signal="pressed" from="Search/SearchNext" to="." method="_on_SearchNext_pressed"] +[connection signal="pressed" from="Search/SearchPrev" to="." method="_on_SearchPrev_pressed"] diff --git a/addons/gut/gui/ResizeHandle.gd b/addons/gut/gui/ResizeHandle.gd new file mode 100644 index 0000000..a79f6e3 --- /dev/null +++ b/addons/gut/gui/ResizeHandle.gd @@ -0,0 +1,107 @@ +@tool +extends ColorRect +# ############################################################################# +# Resize Handle control. Place onto a control. Set the orientation, then +# set the control that this should resize. Then you can resize the control +# by dragging this thing around. It's pretty neat. +# ############################################################################# +enum ORIENTATION { + LEFT, + RIGHT +} + +@export var orientation := ORIENTATION.RIGHT : + get: return orientation + set(val): + orientation = val + queue_redraw() +@export var resize_control : Control = null +@export var vertical_resize := true + +var _line_width = .5 +var _line_color = Color(.4, .4, .4) +var _active_line_color = Color(.3, .3, .3) +var _invalid_line_color = Color(1, 0, 0) + +var _line_space = 3 +var _num_lines = 8 + +var _mouse_down = false +# Called when the node enters the scene tree for the first time. + + +func _draw(): + var c = _line_color + if(resize_control == null): + c = _invalid_line_color + elif(_mouse_down): + c = _active_line_color + + if(orientation == ORIENTATION.LEFT): + _draw_resize_handle_left(c) + else: + _draw_resize_handle_right(c) + + +func _gui_input(event): + if(resize_control == null): + return + + if(orientation == ORIENTATION.LEFT): + _handle_left_input(event) + else: + _handle_right_input(event) + + +# Draw the lines in the corner to show where you can +# drag to resize the dialog +func _draw_resize_handle_right(draw_color): + var br = size + + for i in range(_num_lines): + var start = br - Vector2(i * _line_space, 0) + var end = br - Vector2(0, i * _line_space) + draw_line(start, end, draw_color, _line_width, true) + + +func _draw_resize_handle_left(draw_color): + var bl = Vector2(0, size.y) + + for i in range(_num_lines): + var start = bl + Vector2(i * _line_space, 0) + var end = bl - Vector2(0, i * _line_space) + draw_line(start, end, draw_color, _line_width, true) + + +func _handle_right_input(event : InputEvent): + if(event is InputEventMouseMotion): + if(_mouse_down and + event.global_position.x > 0 and + event.global_position.y < DisplayServer.window_get_size().y): + + if(vertical_resize): + resize_control.size.y += event.relative.y + resize_control.size.x += event.relative.x + elif(event is InputEventMouseButton): + if(event.button_index == MOUSE_BUTTON_LEFT): + _mouse_down = event.pressed + queue_redraw() + + +func _handle_left_input(event : InputEvent): + if(event is InputEventMouseMotion): + if(_mouse_down and + event.global_position.x > 0 and + event.global_position.y < DisplayServer.window_get_size().y): + + var start_size = resize_control.size + resize_control.size.x -= event.relative.x + if(resize_control.size.x != start_size.x): + resize_control.global_position.x += event.relative.x + + if(vertical_resize): + resize_control.size.y += event.relative.y + elif(event is InputEventMouseButton): + if(event.button_index == MOUSE_BUTTON_LEFT): + _mouse_down = event.pressed + queue_redraw() diff --git a/addons/gut/gui/ResizeHandle.gd.uid b/addons/gut/gui/ResizeHandle.gd.uid new file mode 100644 index 0000000..a537485 --- /dev/null +++ b/addons/gut/gui/ResizeHandle.gd.uid @@ -0,0 +1 @@ +uid://duf6rfdqr6yoc diff --git a/addons/gut/gui/ResizeHandle.tscn b/addons/gut/gui/ResizeHandle.tscn new file mode 100644 index 0000000..b13d9e5 --- /dev/null +++ b/addons/gut/gui/ResizeHandle.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3 uid="uid://bvrqqgjpyouse"] + +[ext_resource type="Script" uid="uid://duf6rfdqr6yoc" path="res://addons/gut/gui/ResizeHandle.gd" id="1_oi5ed"] + +[node name="ResizeHandle" type="ColorRect"] +custom_minimum_size = Vector2(20, 20) +color = Color(1, 1, 1, 0) +script = ExtResource("1_oi5ed") diff --git a/addons/gut/gui/ResultsTree.gd b/addons/gut/gui/ResultsTree.gd new file mode 100644 index 0000000..45e5f28 --- /dev/null +++ b/addons/gut/gui/ResultsTree.gd @@ -0,0 +1,368 @@ +@tool +extends Tree + +var _show_orphans = true +var show_orphans = true : + get: return _show_orphans + set(val): _show_orphans = val + + +var _hide_passing = true +var hide_passing = true : + get: return _hide_passing + set(val): _hide_passing = val + + +var _icons = { + red = load('res://addons/gut/images/red.png'), + green = load('res://addons/gut/images/green.png'), + yellow = load('res://addons/gut/images/yellow.png'), +} + +@export var script_entry_color : Color = Color(0, 0, 0, .2) : + set(val): + if(val != null): + script_entry_color = val +@export var column_0_color : Color = Color(1, 1, 1, 0) : + set(val): + if(val != null): + column_0_color = val +@export var column_1_color : Color = Color(0, 0, 0, .2): + set(val): + if(val != null): + column_1_color = val + + + +var _max_icon_width = 10 +var _root : TreeItem + + +@onready var lbl_overlay = $TextOverlay + + +signal selected(script_path, inner_class, test_name, line_number) + +func _debug_ready(): + hide_passing = false + load_json_file('user://gut_temp_directory/gut_editor.json') + + +func _ready(): + _root = create_item() + set_hide_root(true) + columns = 2 + set_column_expand(0, true) + set_column_expand_ratio(0, 5) + + set_column_expand_ratio(1, 1) + set_column_expand(1, true) + + item_selected.connect(_on_tree_item_selected) + + if(get_parent() == get_tree().root): + _debug_ready() + + +# ------------------- +# Private +# ------------------- +func _get_line_number_from_assert_msg(msg): + var line = -1 + if(msg.find('at line') > 0): + line = msg.split("at line")[-1].split(" ")[-1].to_int() + return line + + +func _get_path_and_inner_class_name_from_test_path(path): + var to_return = { + path = '', + inner_class = '' + } + + to_return.path = path + if !path.ends_with('.gd'): + var loc = path.find('.gd') + to_return.inner_class = path.split('.')[-1] + to_return.path = path.substr(0, loc + 3) + return to_return + + +func _find_script_item_with_path(path): + var items = _root.get_children() + var to_return = null + + var idx = 0 + while(idx < items.size() and to_return == null): + var item = items[idx] + if(item.get_metadata(0).path == path): + to_return = item + else: + idx += 1 + + return to_return + + +func _add_script_tree_item(script_path, script_json): + var path_info = _get_path_and_inner_class_name_from_test_path(script_path) + var item_text = script_path + var parent = _root + + if(path_info.inner_class != ''): + parent = _find_script_item_with_path(path_info.path) + item_text = path_info.inner_class + if(parent == null): + parent = _add_script_tree_item(path_info.path, {}) + + var item = create_item(parent) + item.set_text(0, item_text) + var meta = { + "type":"script", + "path":path_info.path, + "inner_class":path_info.inner_class, + "json":script_json, + "inner_passing":0, + "inner_tests":0 + } + item.set_metadata(0, meta) + item.set_custom_bg_color(0, script_entry_color) + item.set_custom_bg_color(1, script_entry_color) + + return item + + +func _add_assert_item(text, icon, parent_item): + # print(' * adding assert') + var assert_item = create_item(parent_item) + assert_item.set_icon_max_width(0, _max_icon_width) + assert_item.set_text(0, text) + assert_item.set_metadata(0, {"type":"assert"}) + assert_item.set_icon(0, icon) + assert_item.set_custom_bg_color(0, column_0_color) + assert_item.set_custom_bg_color(1, column_1_color) + + return assert_item + + +func _add_test_tree_item(test_name, test_json, script_item): + # print(' * adding test ', test_name) + var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphan_count == 0) + if(_hide_passing and test_json['status'] == 'pass' and no_orphans_to_show): + return + + var item = create_item(script_item) + var status = test_json['status'] + var meta = {"type":"test", "json":test_json} + + item.set_text(0, test_name) + item.set_text(1, status) + item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT) + item.set_custom_bg_color(1, column_1_color) + + item.set_metadata(0, meta) + item.set_icon_max_width(0, _max_icon_width) + item.set_custom_bg_color(0, column_0_color) + + if(status == 'pass' and no_orphans_to_show): + item.set_icon(0, _icons.green) + elif(status == 'fail'): + item.set_icon(0, _icons.red) + else: + item.set_icon(0, _icons.yellow) + + if(!_hide_passing): + for passing in test_json.passing: + _add_assert_item('pass: ' + passing, _icons.green, item) + + for failure in test_json.failing: + _add_assert_item("fail: " + failure.replace("\n", ''), _icons.red, item) + + for pending in test_json.pending: + _add_assert_item("pending: " + pending.replace("\n", ''), _icons.yellow, item) + + var orphan_text = 'orphans' + if(test_json.orphan_count == 1): + orphan_text = 'orphan' + orphan_text = str(int(test_json.orphan_count), ' ', orphan_text) + + if(!no_orphans_to_show): + var orphan_item = _add_assert_item(orphan_text, _icons.yellow, item) + for o in test_json.orphans: + var orphan_entry = create_item(orphan_item) + orphan_entry.set_text(0, o) + orphan_entry.set_custom_bg_color(0, column_0_color) + orphan_entry.set_custom_bg_color(1, column_1_color) + + return item + + +func _add_script_to_tree(key, script_json): + var tests = script_json['tests'] + var test_keys = tests.keys() + var s_item = _add_script_tree_item(key, script_json) + var bad_count = 0 + + for test_key in test_keys: + var t_item = _add_test_tree_item(test_key, tests[test_key], s_item) + if(tests[test_key].status != 'pass'): + bad_count += 1 + elif(t_item != null): + t_item.collapsed = true + + if(s_item.get_children().size() == 0): + if(script_json.props.skipped): + _add_assert_item("Skipped", _icons.yellow, s_item) + s_item.set_text(1, "Skipped") + else: + s_item.free() + else: + var total_text = str('All ', test_keys.size(), ' passed') + if(bad_count == 0): + s_item.collapsed = true + else: + total_text = str(int(test_keys.size() - bad_count), '/', int(test_keys.size()), ' passed') + s_item.set_text(1, total_text) + + +func _free_childless_scripts(): + var items = _root.get_children() + for item in items: + var next_item = item.get_next() + if(item.get_children().size() == 0): + item.free() + item = next_item + + +func _show_all_passed(): + if(_root.get_children().size() == 0): + add_centered_text('Everything passed!') + + +func _load_result_tree(j): + var scripts = j['test_scripts']['scripts'] + var script_keys = scripts.keys() + # if we made it here, the json is valid and we did something, otherwise the + # 'nothing to see here' should be visible. + clear_centered_text() + + var add_count = 0 + for key in script_keys: + add_count += 1 + _add_script_to_tree(key, scripts[key]) + + _free_childless_scripts() + if(add_count == 0): + add_centered_text('Nothing was run') + else: + _show_all_passed() +# ------------------- +# Events +# ------------------- +func _on_tree_item_selected(): + var item = get_selected() + var item_meta = item.get_metadata(0) + var item_type = null + + # Only select the left side of the tree item, cause I like that better. + # you can still click the right, but only the left gets highlighted. + if(item.is_selected(1)): + item.deselect(1) + item.select(0) + + if(item_meta == null): + return + else: + item_type = item_meta.type + + var script_path = ''; + var line = -1; + var test_name = '' + var inner_class = '' + + if(item_type == 'test'): + var s_item = item.get_parent() + script_path = s_item.get_metadata(0)['path'] + inner_class = s_item.get_metadata(0)['inner_class'] + line = -1 + test_name = item.get_text(0) + elif(item_type == 'assert'): + var s_item = item.get_parent().get_parent() + script_path = s_item.get_metadata(0)['path'] + inner_class = s_item.get_metadata(0)['inner_class'] + line = _get_line_number_from_assert_msg(item.get_text(0)) + test_name = item.get_parent().get_text(0) + elif(item_type == 'script'): + script_path = item.get_metadata(0)['path'] + if(item.get_parent() != _root): + inner_class = item.get_text(0) + line = -1 + test_name = '' + else: + return + + selected.emit(script_path, inner_class, test_name, line) + + +# ------------------- +# Public +# ------------------- +func load_json_file(path): + var file = FileAccess.open(path, FileAccess.READ) + var text = '' + if(file != null): + text = file.get_as_text() + + if(text != ''): + var test_json_conv = JSON.new() + var result = test_json_conv.parse(text) + if(result != OK): + add_centered_text(str(path, " has invalid json in it \n", + 'Error ', result, "@", test_json_conv.get_error_line(), "\n", + test_json_conv.get_error_message())) + return + + var data = test_json_conv.get_data() + load_json_results(data) + else: + add_centered_text(str(path, ' was empty or does not exist.')) + + +func load_json_results(j): + clear() + if(_root == null): + _root = create_item() + + _load_result_tree(j) + + +#func clear(): + #clear() + #_root = create_item() + + +func set_summary_min_width(width): + set_column_custom_minimum_width(1, width) + + +func add_centered_text(t): + lbl_overlay.visible = true + lbl_overlay.text = t + + +func clear_centered_text(): + lbl_overlay.visible = false + lbl_overlay.text = '' + + +func collapse_all(): + set_collapsed_on_all(_root, true) + + +func expand_all(): + set_collapsed_on_all(_root, false) + + +func set_collapsed_on_all(item, value): + item.set_collapsed_recursive(value) + if(item == _root and value): + item.set_collapsed(false) diff --git a/addons/gut/gui/ResultsTree.gd.uid b/addons/gut/gui/ResultsTree.gd.uid new file mode 100644 index 0000000..dee3727 --- /dev/null +++ b/addons/gut/gui/ResultsTree.gd.uid @@ -0,0 +1 @@ +uid://dehdhn78qv5tr diff --git a/addons/gut/gui/ResultsTree.tscn b/addons/gut/gui/ResultsTree.tscn new file mode 100644 index 0000000..8868f87 --- /dev/null +++ b/addons/gut/gui/ResultsTree.tscn @@ -0,0 +1,34 @@ +[gd_scene format=3 uid="uid://dls5r5f6157nq"] + +[ext_resource type="Script" uid="uid://dehdhn78qv5tr" path="res://addons/gut/gui/ResultsTree.gd" id="1_b4uub"] + +[node name="ResultsTree" type="Tree" unique_id=1240770657] +offset_right = 1082.0 +offset_bottom = 544.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +columns = 2 +hide_root = true +script = ExtResource("1_b4uub") + +[node name="TextOverlay" type="Label" parent="." unique_id=1487654929] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ResultsTree" type="VBoxContainer" parent="." unique_id=87055617] +custom_minimum_size = Vector2(10, 10) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -70.0 +offset_bottom = -104.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/addons/gut/gui/RunAtCursor.gd b/addons/gut/gui/RunAtCursor.gd new file mode 100644 index 0000000..57475df --- /dev/null +++ b/addons/gut/gui/RunAtCursor.gd @@ -0,0 +1,171 @@ +@tool +extends Control + +var EditorCaretContextNotifier = load('res://addons/gut/editor_caret_context_notifier.gd') + +@onready var _ctrls = { + btn_script = $HBox/BtnRunScript, + btn_inner = $HBox/BtnRunInnerClass, + btn_method = $HBox/BtnRunMethod, + lbl_none = $HBox/LblNoneSelected, + arrow_1 = $HBox/Arrow1, + arrow_2 = $HBox/Arrow2 +} + +var _caret_notifier = null + +var _last_info = { + script = null, + inner_class = null, + method = null +} + +var disabled = false : + set(val): + disabled = val + if(is_inside_tree()): + _ctrls.btn_script.disabled = val + _ctrls.btn_inner.disabled = val + _ctrls.btn_method.disabled = val +var method_prefix = 'test_' +var inner_class_prefix = 'Test' +var menu_manager = null : + set(val): + menu_manager = val + menu_manager.run_script.connect(_on_BtnRunScript_pressed) + menu_manager.run_at_cursor.connect(run_at_cursor) + menu_manager.rerun.connect(rerun) + menu_manager.run_inner_class.connect(_on_BtnRunInnerClass_pressed) + menu_manager.run_test.connect(_on_BtnRunMethod_pressed) + _update_buttons(_last_info) + + +signal run_tests(what) + + +func _ready(): + _ctrls.lbl_none.visible = true + _ctrls.btn_script.visible = false + _ctrls.btn_inner.visible = false + _ctrls.btn_method.visible = false + _ctrls.arrow_1.visible = false + _ctrls.arrow_2.visible = false + + _caret_notifier = EditorCaretContextNotifier.new() + add_child(_caret_notifier) + _caret_notifier.it_changed.connect(_on_caret_notifer_changed) + + disabled = disabled + + +func _on_caret_notifer_changed(data): + if(data.is_test_script): + _last_info = data + _update_buttons(_last_info) + + +# ---------------- +# Private +# ---------------- + +func _update_buttons(info): + _ctrls.lbl_none.visible = false + _ctrls.btn_script.visible = info.script != null + + if(info.script != null and info.is_test_script): + _ctrls.btn_script.text = info.script.resource_path.get_file() + + _ctrls.btn_inner.visible = info.inner_class != null + _ctrls.arrow_1.visible = info.inner_class != null + _ctrls.btn_inner.text = str(info.inner_class) + _ctrls.btn_inner.tooltip_text = str("Run all tests in Inner-Test-Class ", info.inner_class) + + var is_test_method = info.method != null and info.method.begins_with(method_prefix) + _ctrls.btn_method.visible = is_test_method + _ctrls.arrow_2.visible = is_test_method + if(is_test_method): + _ctrls.btn_method.text = str(info.method) + _ctrls.btn_method.tooltip_text = str("Run test ", info.method) + + if(menu_manager != null): + menu_manager.disable_menu("run_script", info.script == null) + menu_manager.disable_menu("run_inner_class", info.inner_class == null) + menu_manager.disable_menu("run_at_cursor", info.script == null) + menu_manager.disable_menu("run_test", is_test_method) + menu_manager.disable_menu("rerun", _last_run_info == {}) + # The button's new size won't take effect until the next frame. + # This appears to be what was causing the button to not be clickable the + # first time. + _update_size.call_deferred() + + +func _update_size(): + custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x + +var _last_run_info = {} +func _emit_run_tests(info): + _last_run_info = info.duplicate() + run_tests.emit(info) + +# ---------------- +# Events +# ---------------- +func _on_BtnRunScript_pressed(): + var info = _last_info.duplicate() + info.script = info.script.resource_path.get_file() + info.inner_class = null + info.method = null + _emit_run_tests(info) + + +func _on_BtnRunInnerClass_pressed(): + var info = _last_info.duplicate() + info.script = info.script.resource_path.get_file() + info.method = null + _emit_run_tests(info) + + +func _on_BtnRunMethod_pressed(): + var info = _last_info.duplicate() + info.script = info.script.resource_path.get_file() + _emit_run_tests(info) + + +# ---------------- +# Public +# ---------------- +func rerun(): + if(_last_run_info != {}): + _emit_run_tests(_last_run_info) + + +func run_at_cursor(): + if(_ctrls.btn_method.visible): + _on_BtnRunMethod_pressed() + elif(_ctrls.btn_inner.visible): + _on_BtnRunInnerClass_pressed() + elif(_ctrls.btn_script.visible): + _on_BtnRunScript_pressed() + else: + print("nothing selected") + + +func get_script_button(): + return _ctrls.btn_script + + +func get_inner_button(): + return _ctrls.btn_inner + + +func get_test_button(): + return _ctrls.btn_method + + +func set_inner_class_prefix(value): + _caret_notifier.inner_class_prefix = value + + +func apply_gut_config(gut_config): + _caret_notifier.script_prefix = gut_config.options.prefix + _caret_notifier.script_suffix = gut_config.options.suffix diff --git a/addons/gut/gui/RunAtCursor.gd.uid b/addons/gut/gui/RunAtCursor.gd.uid new file mode 100644 index 0000000..944da03 --- /dev/null +++ b/addons/gut/gui/RunAtCursor.gd.uid @@ -0,0 +1 @@ +uid://c4gmgdl1xwflw diff --git a/addons/gut/gui/RunAtCursor.tscn b/addons/gut/gui/RunAtCursor.tscn new file mode 100644 index 0000000..8fa5886 --- /dev/null +++ b/addons/gut/gui/RunAtCursor.tscn @@ -0,0 +1,60 @@ +[gd_scene load_steps=3 format=3 uid="uid://0yunjxtaa8iw"] + +[ext_resource type="Script" uid="uid://c4gmgdl1xwflw" path="res://addons/gut/gui/RunAtCursor.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://6wra5rxmfsrl" path="res://addons/gut/gui/arrow.png" id="3"] + +[node name="RunAtCursor" type="Control"] +custom_minimum_size = Vector2(510, 0) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 1.0 +offset_bottom = -527.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="HBox" type="HBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="LblNoneSelected" type="Label" parent="HBox"] +visible = false +layout_mode = 2 +text = "" + +[node name="BtnRunScript" type="Button" parent="HBox"] +layout_mode = 2 +text = "test_test.gd" + +[node name="Arrow1" type="TextureButton" parent="HBox"] +custom_minimum_size = Vector2(24, 0) +layout_mode = 2 +texture_normal = ExtResource("3") +stretch_mode = 3 + +[node name="BtnRunInnerClass" type="Button" parent="HBox"] +layout_mode = 2 +tooltip_text = "Run all tests in Inner-Test-Class TestAssertNe" +text = "TestAssertNe" + +[node name="Arrow2" type="TextureButton" parent="HBox"] +custom_minimum_size = Vector2(24, 0) +layout_mode = 2 +texture_normal = ExtResource("3") +stretch_mode = 3 + +[node name="BtnRunMethod" type="Button" parent="HBox"] +layout_mode = 2 +tooltip_text = "Run test test_fails_with_integers_equal" +text = "test_fails_with_integers_equal" + +[connection signal="pressed" from="HBox/BtnRunScript" to="." method="_on_BtnRunScript_pressed"] +[connection signal="pressed" from="HBox/BtnRunInnerClass" to="." method="_on_BtnRunInnerClass_pressed"] +[connection signal="pressed" from="HBox/BtnRunMethod" to="." method="_on_BtnRunMethod_pressed"] diff --git a/addons/gut/gui/RunExternally.gd b/addons/gut/gui/RunExternally.gd new file mode 100644 index 0000000..d98571b --- /dev/null +++ b/addons/gut/gui/RunExternally.gd @@ -0,0 +1,216 @@ +@tool +extends Control + +# I'm probably going to put this back in later and I don't want to create it +# again. Yeah, yeah, yeah. +# class DotsAnimator: +# var text = '' +# var dot = '.' +# var max_dots = 3 +# var dot_delay = .5 + +# var _anim_text = '' +# var _elapsed_time = 0.0 +# var _cur_dots = 0 + +# func get_animated_text(): +# return _anim_text + +# func add_time(delta): +# _elapsed_time += delta +# if(_elapsed_time > dot_delay): +# _elapsed_time = 0 +# _cur_dots += 1 +# if(_cur_dots > max_dots): +# _cur_dots = 0 + +# _anim_text = text.rpad(text.length() + _cur_dots, dot) + + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + +@onready var btn_kill_it = $BgControl/VBox/Kill +@onready var bg_control = $BgControl + +var _pipe_results = {} +var _debug_mode = false +var _std_thread : Thread +var _escape_regex : RegEx = RegEx.new() +var _text_buffer = '' + +var bottom_panel = null : + set(val): + bottom_panel = val + bottom_panel.resized.connect(_on_bottom_panel_resized) +var blocking_mode = "Blocking" +var additional_arguments = [] +var remove_escape_characters = true +@export var bg_color = Color.WHITE: + set(val): + bg_color = val + if(is_inside_tree()): + bg_control.get("theme_override_styles/panel").bg_color = bg_color + + +func _debug_ready(): + _debug_mode = true + additional_arguments = ['-gselect', 'test_awaiter.gd', '-gconfig', 'res://.gutconfig.json'] # '-gunit_test_name', 'test_can_clear_spies' + blocking_mode = "NonBlocking" + run_tests() + + +func _ready(): + _escape_regex.compile("\\x1b\\[[0-9;]*m") + btn_kill_it.visible = false + + if(get_parent() == get_tree().root): + _debug_ready.call_deferred() + bg_color = bg_color + + +func _process(_delta: float) -> void: + if(_pipe_results != {}): + if(!OS.is_process_running(_pipe_results.pid)): + _end_non_blocking() + + +# ---------- +# Private +# ---------- +func _center_me(): + position = get_parent().size / 2.0 - size / 2.0 + + +func _output_text(text, should_scroll = true): + if(_debug_mode): + print(text) + else: + if(remove_escape_characters): + text = _escape_regex.sub(text, '', true) + + if(bottom_panel != null): + bottom_panel.add_output_text(text) + if(should_scroll): + _scroll_output_pane(-1) + else: + _text_buffer += text + + +func _scroll_output_pane(line): + if(!_debug_mode and bottom_panel != null): + var txt_ctrl = bottom_panel.get_text_output_control().get_rich_text_edit() + if(line == -1): + line = txt_ctrl.get_line_count() + txt_ctrl.scroll_vertical = line + + +func _add_arguments_to_output(): + if(additional_arguments.size() != 0): + _output_text( + str("Run Mode arguments: ", ' '.join(additional_arguments), "\n\n") + ) + + +func _load_json(): + if(_debug_mode): + pass # could load file and print it if we want. + elif(bottom_panel != null): + bottom_panel.load_result_json() + + +func _run_blocking(options): + btn_kill_it.visible = false + var output = [] + await get_tree().create_timer(.1).timeout + + OS.execute(OS.get_executable_path(), options, output, true) + + _output_text(output[0]) + _add_arguments_to_output() + _scroll_output_pane(-1) + + _load_json() + queue_free() + + +func _read_non_blocking_stdio(): + while(OS.is_process_running(_pipe_results.pid)): + while(_pipe_results.stderr.get_length() > 0): + _output_text(_pipe_results.stderr.get_line() + "\n") + + while(_pipe_results.stdio.get_length() > 0): + _output_text(_pipe_results.stdio.get_line() + "\n") + + # without this, things start to lock up. + await get_tree().process_frame + + +func _run_non_blocking(options): + _pipe_results = OS.execute_with_pipe(OS.get_executable_path(), options, false) + _std_thread = Thread.new() + _std_thread.start(_read_non_blocking_stdio) + btn_kill_it.visible = true + + +func _end_non_blocking(): + _add_arguments_to_output() + _scroll_output_pane(-1) + + _load_json() + + _pipe_results = {} + _std_thread.wait_to_finish() + _std_thread = null + queue_free() + if(_debug_mode): + get_tree().quit() + + + +# ---------------- +# Events +# ---------------- +func _on_kill_pressed() -> void: + if(_pipe_results != {} and OS.is_process_running(_pipe_results.pid)): + OS.kill(_pipe_results.pid) + btn_kill_it.visible = false + + +func _on_color_rect_gui_input(event: InputEvent) -> void: + if(event is InputEventMouseMotion): + if(event.button_mask == MOUSE_BUTTON_MASK_LEFT): + position += event.relative + + +func _on_bottom_panel_resized(): + _center_me() + + +# ---------------- +# Public +# ---------------- +func run_tests(): + _center_me() + + var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-graie", "-gdisable_colors", + "-gconfig", GutEditorGlobals.editor_run_gut_config_path] + options.append_array(additional_arguments) + + if(blocking_mode == 'Blocking'): + _run_blocking(options) + else: + _run_non_blocking(options) + + +func get_godot_help(): + _text_buffer = '' + var options = ["--help", "--headless"] + await _run_blocking(options) + return _text_buffer + + +func get_gut_help(): + _text_buffer = '' + var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-gh", "--headless"] + await _run_blocking(options) + return _text_buffer diff --git a/addons/gut/gui/RunExternally.gd.uid b/addons/gut/gui/RunExternally.gd.uid new file mode 100644 index 0000000..2be6465 --- /dev/null +++ b/addons/gut/gui/RunExternally.gd.uid @@ -0,0 +1 @@ +uid://bi8pg352un4om diff --git a/addons/gut/gui/RunExternally.tscn b/addons/gut/gui/RunExternally.tscn new file mode 100644 index 0000000..c72926d --- /dev/null +++ b/addons/gut/gui/RunExternally.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=3 format=3 uid="uid://cftcb0e6g7tu1"] + +[ext_resource type="Script" uid="uid://bi8pg352un4om" path="res://addons/gut/gui/RunExternally.gd" id="1_lrqqi"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_haowt"] +bg_color = Color(0.025935074, 0.17817128, 0.30283752, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 +shadow_size = 5 +shadow_offset = Vector2(7, 7) + +[node name="DoShellOut" type="Control"] +layout_mode = 3 +anchors_preset = 0 +offset_right = 774.0 +offset_bottom = 260.0 +script = ExtResource("1_lrqqi") +bg_color = Color(0.025935074, 0.17817128, 0.30283752, 1) + +[node name="BgControl" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_haowt") + +[node name="VBox" type="VBoxContainer" parent="BgControl"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Spacer" type="CenterContainer" parent="BgControl/VBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Title" type="Label" parent="BgControl/VBox"] +layout_mode = 2 +text = "Running Tests" +horizontal_alignment = 1 + +[node name="Spacer2" type="CenterContainer" parent="BgControl/VBox"] +visible = false +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Kill" type="Button" parent="BgControl/VBox"] +visible = false +custom_minimum_size = Vector2(200, 50) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Stop" + +[node name="Spacer3" type="CenterContainer" parent="BgControl/VBox"] +layout_mode = 2 +size_flags_vertical = 3 + +[connection signal="gui_input" from="BgControl" to="." method="_on_color_rect_gui_input"] +[connection signal="pressed" from="BgControl/VBox/Kill" to="." method="_on_kill_pressed"] diff --git a/addons/gut/gui/RunResults.gd b/addons/gut/gui/RunResults.gd new file mode 100644 index 0000000..83c9f5e --- /dev/null +++ b/addons/gut/gui/RunResults.gd @@ -0,0 +1,250 @@ +@tool +extends Control + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + +var _interface = null +var _output_control = null + +@onready var _ctrls = { + tree = $VBox/Output/Scroll/Tree, + toolbar = { + toolbar = $VBox/Toolbar, + collapse = $VBox/Toolbar/Collapse, + collapse_all = $VBox/Toolbar/CollapseAll, + expand = $VBox/Toolbar/Expand, + expand_all = $VBox/Toolbar/ExpandAll, + hide_passing = $VBox/Toolbar/HidePassing, + show_script = $VBox/Toolbar/ShowScript, + scroll_output = $VBox/Toolbar/ScrollOutput + } +} + +func _ready(): + if(get_parent() is SubViewport): + return + + var f = null + if ($FontSampler.get_label_settings() == null) : + f = get_theme_default_font() + else : + f = $FontSampler.get_label_settings().font + var s_size = f.get_string_size("000 of 000 passed") + _ctrls.tree.set_summary_min_width(s_size.x) + + _set_toolbutton_icon(_ctrls.toolbar.collapse, 'CollapseTree', 'c') + _set_toolbutton_icon(_ctrls.toolbar.collapse_all, 'CollapseTree', 'c') + _set_toolbutton_icon(_ctrls.toolbar.expand, 'ExpandTree', 'e') + _set_toolbutton_icon(_ctrls.toolbar.expand_all, 'ExpandTree', 'e') + _set_toolbutton_icon(_ctrls.toolbar.show_script, 'Script', 'ss') + _set_toolbutton_icon(_ctrls.toolbar.scroll_output, 'Font', 'so') + + _ctrls.tree.hide_passing = true + _ctrls.toolbar.hide_passing.button_pressed = false + _ctrls.tree.show_orphans = true + _ctrls.tree.selected.connect(_on_item_selected) + + if(get_parent() == get_tree().root): + _test_running_setup() + + call_deferred('_update_min_width') + + +func _test_running_setup(): + _ctrls.tree.hide_passing = true + _ctrls.tree.show_orphans = true + + _ctrls.toolbar.hide_passing.text = '[hp]' + _ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path) + + +func _set_toolbutton_icon(btn, icon_name, text): + if(Engine.is_editor_hint()): + btn.icon = get_theme_icon(icon_name, 'EditorIcons') + else: + btn.text = str('[', text, ']') + + +func _update_min_width(): + custom_minimum_size.x = _ctrls.toolbar.toolbar.size.x + + +func _open_script_in_editor(path, line_number): + if(_interface == null): + print('Too soon, wait a bit and try again.') + return + + var r = load(path) + if(line_number != null and line_number != -1): + _interface.edit_script(r, line_number) + else: + _interface.edit_script(r) + + if(_ctrls.toolbar.show_script.pressed): + _interface.set_main_screen_editor('Script') + + +# starts at beginning of text edit and searches for each search term, moving +# through the text as it goes; ensuring that, when done, it found the first +# occurance of the last srting that happend after the first occurance of +# each string before it. (Generic way of searching for a method name in an +# inner class that may have be a duplicate of a method name in a different +# inner class) +func _get_line_number_for_seq_search(search_strings, te): + if(te == null): + print("No Text editor to get line number for") + return 0; + + var result = null + var line = Vector2i(0, 0) + var s_flags = 0 + + var i = 0 + var string_found = true + while(i < search_strings.size() and string_found): + result = te.search(search_strings[i], s_flags, line.y, line.x) + if(result.x != -1): + line = result + else: + string_found = false + i += 1 + + return line.y + + +func _goto_code(path, line, method_name='', inner_class =''): + if(_interface == null): + print('going to ', [path, line, method_name, inner_class]) + return + + _open_script_in_editor(path, line) + if(line == -1): + var search_strings = [] + if(inner_class != ''): + search_strings.append(inner_class) + + if(method_name != ''): + search_strings.append(method_name) + + await get_tree().process_frame + line = _get_line_number_for_seq_search(search_strings, + _interface.get_script_editor().get_current_editor().get_base_editor()) + if(line != null and line != -1): + _interface.get_script_editor().goto_line(line) + + +func _goto_output(path, method_name, inner_class): + if(_output_control == null): + return + + var search_strings = [path] + + if(inner_class != ''): + search_strings.append(inner_class) + + if(method_name != ''): + search_strings.append(method_name) + + var line = _get_line_number_for_seq_search(search_strings, _output_control.get_rich_text_edit()) + if(line != null and line != -1): + _output_control.scroll_to_line(line) + + + + +# -------------- +# Events +# -------------- +func _on_Collapse_pressed(): + collapse_selected() + + +func _on_Expand_pressed(): + expand_selected() + + +func _on_CollapseAll_pressed(): + collapse_all() + + +func _on_ExpandAll_pressed(): + expand_all() + + +func _on_Hide_Passing_pressed(): + _ctrls.tree.hide_passing = !_ctrls.toolbar.hide_passing.button_pressed + _ctrls.tree.load_json_file(GutEditorGlobals.editor_run_json_results_path) + + +func _on_item_selected(script_path, inner_class, test_name, line): + if(_ctrls.toolbar.show_script.button_pressed): + _goto_code(script_path, line, test_name, inner_class) + if(_ctrls.toolbar.scroll_output.button_pressed): + _goto_output(script_path, test_name, inner_class) + + + + +# -------------- +# Public +# -------------- +func add_centered_text(t): + _ctrls.tree.add_centered_text(t) + + +func clear_centered_text(): + _ctrls.tree.clear_centered_text() + + +func clear(): + _ctrls.tree.clear() + clear_centered_text() + + +func set_interface(which): + _interface = which + + +func collapse_all(): + _ctrls.tree.collapse_all() + + +func expand_all(): + _ctrls.tree.expand_all() + + +func collapse_selected(): + var item = _ctrls.tree.get_selected() + if(item != null): + _ctrls.tree.set_collapsed_on_all(item, true) + + +func expand_selected(): + var item = _ctrls.tree.get_selected() + if(item != null): + _ctrls.tree.set_collapsed_on_all(item, false) + + +func set_show_orphans(should): + _ctrls.tree.show_orphans = should + + +func set_font(font_name, size): + pass +# var dyn_font = FontFile.new() +# var font_data = FontFile.new() +# font_data.font_path = 'res://addons/gut/fonts/' + font_name + '-Regular.ttf' +# font_data.antialiased = true +# dyn_font.font_data = font_data +# +# _font = dyn_font +# _font.size = size +# _font_size = size + + +func set_output_control(value): + _output_control = value + + +func load_json_results(j): + _ctrls.tree.load_json_results(j) diff --git a/addons/gut/gui/RunResults.gd.uid b/addons/gut/gui/RunResults.gd.uid new file mode 100644 index 0000000..27be8b7 --- /dev/null +++ b/addons/gut/gui/RunResults.gd.uid @@ -0,0 +1 @@ +uid://chnko3073tkcv diff --git a/addons/gut/gui/RunResults.tscn b/addons/gut/gui/RunResults.tscn new file mode 100644 index 0000000..2400168 --- /dev/null +++ b/addons/gut/gui/RunResults.tscn @@ -0,0 +1,109 @@ +[gd_scene load_steps=4 format=3 uid="uid://4gyyn12um08h"] + +[ext_resource type="Script" uid="uid://chnko3073tkcv" path="res://addons/gut/gui/RunResults.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="2_1k8e0"] +[ext_resource type="PackedScene" uid="uid://dls5r5f6157nq" path="res://addons/gut/gui/ResultsTree.tscn" id="2_o808v"] + +[node name="RunResults" type="Control"] +custom_minimum_size = Vector2(345, 0) +layout_mode = 3 +anchors_preset = 0 +offset_right = 709.0 +offset_bottom = 321.0 +script = ExtResource("1") + +[node name="VBox" type="VBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Toolbar" type="HBoxContainer" parent="VBox"] +layout_mode = 2 +size_flags_horizontal = 0 + +[node name="Expand" type="Button" parent="VBox/Toolbar"] +visible = false +layout_mode = 2 +icon = ExtResource("2_1k8e0") + +[node name="Collapse" type="Button" parent="VBox/Toolbar"] +visible = false +layout_mode = 2 +icon = ExtResource("2_1k8e0") + +[node name="Sep" type="ColorRect" parent="VBox/Toolbar"] +visible = false +custom_minimum_size = Vector2(2, 0) +layout_mode = 2 + +[node name="LblAll" type="Label" parent="VBox/Toolbar"] +visible = false +layout_mode = 2 +text = "All:" + +[node name="ExpandAll" type="Button" parent="VBox/Toolbar"] +layout_mode = 2 +icon = ExtResource("2_1k8e0") + +[node name="CollapseAll" type="Button" parent="VBox/Toolbar"] +layout_mode = 2 +icon = ExtResource("2_1k8e0") + +[node name="Sep2" type="ColorRect" parent="VBox/Toolbar"] +custom_minimum_size = Vector2(2, 0) +layout_mode = 2 + +[node name="HidePassing" type="CheckBox" parent="VBox/Toolbar"] +layout_mode = 2 +size_flags_horizontal = 4 +text = "Passing" + +[node name="Sep3" type="ColorRect" parent="VBox/Toolbar"] +custom_minimum_size = Vector2(2, 0) +layout_mode = 2 + +[node name="LblSync" type="Label" parent="VBox/Toolbar"] +layout_mode = 2 +text = "Sync:" + +[node name="ShowScript" type="Button" parent="VBox/Toolbar"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +icon = ExtResource("2_1k8e0") + +[node name="ScrollOutput" type="Button" parent="VBox/Toolbar"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +icon = ExtResource("2_1k8e0") + +[node name="Output" type="Panel" parent="VBox"] +self_modulate = Color(1, 1, 1, 0.541176) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Scroll" type="ScrollContainer" parent="VBox/Output"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Tree" parent="VBox/Output/Scroll" instance=ExtResource("2_o808v")] +layout_mode = 2 + +[node name="FontSampler" type="Label" parent="."] +visible = false +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 14.0 +text = "000 of 000 passed" + +[connection signal="pressed" from="VBox/Toolbar/Expand" to="." method="_on_Expand_pressed"] +[connection signal="pressed" from="VBox/Toolbar/Collapse" to="." method="_on_Collapse_pressed"] +[connection signal="pressed" from="VBox/Toolbar/ExpandAll" to="." method="_on_ExpandAll_pressed"] +[connection signal="pressed" from="VBox/Toolbar/CollapseAll" to="." method="_on_CollapseAll_pressed"] +[connection signal="pressed" from="VBox/Toolbar/HidePassing" to="." method="_on_Hide_Passing_pressed"] diff --git a/addons/gut/gui/Settings.tscn b/addons/gut/gui/Settings.tscn new file mode 100644 index 0000000..d827740 --- /dev/null +++ b/addons/gut/gui/Settings.tscn @@ -0,0 +1,7 @@ +[gd_scene format=3 uid="uid://cvvvtsah38l0e"] + +[node name="Settings" type="VBoxContainer"] +offset_right = 388.0 +offset_bottom = 586.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/addons/gut/gui/ShellOutOptions.gd b/addons/gut/gui/ShellOutOptions.gd new file mode 100644 index 0000000..a101fca --- /dev/null +++ b/addons/gut/gui/ShellOutOptions.gd @@ -0,0 +1,321 @@ +@tool +extends ConfirmationDialog + +const RUN_MODE_EDITOR = 'Editor' +const RUN_MODE_BLOCKING = 'Blocking' +const RUN_MODE_NON_BLOCKING = 'NonBlocking' + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + +@onready var _bad_arg_dialog = $AcceptDialog +@onready var _main_container = $ScrollContainer/VBoxContainer + +var _blurb_style_box = StyleBoxEmpty.new() +var _opt_maker_setup = false +var _arg_vbox : VBoxContainer = null +var _my_ok_button : Button = null + +# Run mode button stuff +var _run_mode_theme = load('res://addons/gut/gui/EditorRadioButton.tres') +var _button_group = ButtonGroup.new() +var _btn_in_editor : Button = null +var _btn_blocking : Button = null +var _btn_non_blocking : Button = null +var _txt_additional_arguments = null +var _btn_godot_help = null +var _btn_gut_help = null + + +var opt_maker = null +var default_path = GutEditorGlobals.run_externally_options_path +# I like this. It holds values loaded/saved which makes for an easy +# reset mechanism. Hit OK; values get written to this object (not the file +# system). Hit Cancel; values are reloaded from this object. Call the +# save/load methods to interact with the file system. +# +# Downside: If the keys/sections in the config file change, this ends up +# preserving old data. So you gotta find a way to clean it out +# somehow. +# Downside solved: Clear the config file at the start of the save method. +var _config_file = ConfigFile.new() + +var _run_mode = RUN_MODE_EDITOR +var run_mode = _run_mode: + set(val): + _run_mode = val + if(is_inside_tree()): + _btn_in_editor.button_pressed = _run_mode == RUN_MODE_EDITOR + if(_btn_in_editor.button_pressed): + _btn_in_editor.pressed.emit() + _btn_blocking.button_pressed = _run_mode == RUN_MODE_BLOCKING + if(_btn_blocking.button_pressed): + _btn_blocking.pressed.emit() + _btn_non_blocking.button_pressed = _run_mode == RUN_MODE_NON_BLOCKING + if(_btn_non_blocking.button_pressed): + _btn_non_blocking.pressed.emit() + get(): + return _run_mode + + +var additional_arguments = '' : + get(): + if(_opt_maker_setup): + return opt_maker.controls.additional_arguments.value + else: + return additional_arguments + + +func _debug_ready(): + popup_centered() + default_path = GutEditorGlobals.temp_directory.path_join('test_external_run_options.cfg') + exclusive = false + + var save_btn = Button.new() + save_btn.text = 'save' + save_btn.pressed.connect(func(): + save_to_file() + print(_config_file.encode_to_text())) + save_btn.position = Vector2(100, 20) + save_btn.size = Vector2(100, 100) + get_tree().root.add_child(save_btn) + + var load_btn = Button.new() + load_btn.text = 'load' + load_btn.pressed.connect(func(): + load_from_file() + print(_config_file.encode_to_text())) + load_btn.position = Vector2(100, 130) + load_btn.size = Vector2(100, 100) + get_tree().root.add_child(load_btn) + + var show_btn = Button.new() + show_btn.text = 'Show' + show_btn.pressed.connect(popup_centered) + show_btn.position = Vector2(100, 250) + show_btn.size = Vector2(100, 100) + get_tree().root.add_child(show_btn) + + +func _ready(): + opt_maker = GutUtils.OptionMaker.new(_main_container) + _add_controls() + + if(get_parent() == get_tree().root): + _debug_ready.call_deferred() + + _my_ok_button = Button.new() + _my_ok_button.text = 'OK' + _my_ok_button.pressed.connect(_validate_and_confirm) + get_ok_button().add_sibling(_my_ok_button) + get_ok_button().modulate.a = 0.0 + get_ok_button().text = '' + get_ok_button().disabled = true + + canceled.connect(reset) + _button_group.pressed.connect(_on_mode_button_pressed) + run_mode = run_mode + + +func _validate_and_confirm(): + if(validate_arguments()): + _save_to_config_file(_config_file) + confirmed.emit() + hide() + else: + var dlg_text = str("Invalid arguments. The following cannot be used:\n", + ' '.join(_invalid_args)) + + if(run_mode == RUN_MODE_BLOCKING): + dlg_text += str("\nThese cannot be used with blocking mode:\n", + ' '.join(_invalid_blocking_args)) + + _bad_arg_dialog.dialog_text = dlg_text + _bad_arg_dialog.popup_centered() + + +func _on_mode_button_pressed(which): + if(which == _btn_in_editor): + _arg_vbox.modulate.a = .3 + else: + _arg_vbox.modulate.a = 1.0 + + _txt_additional_arguments.value_ctrl.editable = which != _btn_in_editor + if(which == _btn_in_editor): + _run_mode = RUN_MODE_EDITOR + elif(which == _btn_blocking): + _run_mode = RUN_MODE_BLOCKING + elif(which == _btn_non_blocking): + _run_mode = RUN_MODE_NON_BLOCKING + + +func _add_run_mode_button(text, desc_label, description): + var btn = Button.new() + btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + btn.toggle_mode = true + btn.text = text + btn.button_group = _button_group + btn.theme = _run_mode_theme + btn.pressed.connect(func(): desc_label.text = str('[b]', text, "[/b]\n", description)) + + return btn + + +func _add_blurb(text): + var ctrl = opt_maker.add_blurb(text) + ctrl.set("theme_override_styles/normal", _blurb_style_box) + return ctrl + + +func _add_title(text): + var ctrl = opt_maker.add_title(text) + ctrl.get_child(0).horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + return ctrl + + +func _add_controls(): + _add_title("Run Modes") + _add_blurb( + "Choose how GUT will launch tests. Normally you just run them through the editor, but now " + + "you can run them externally. This is an experimental feature. It has been tested on Mac " + + "and Windows. Your results may vary. Feedback welcome at [url]https://github.com/bitwes/Gut/issues[/url].\n ") + + var button_desc_box = HBoxContainer.new() + var button_box = VBoxContainer.new() + var button_desc = RichTextLabel.new() + button_desc.fit_content = true + button_desc.bbcode_enabled = true + button_desc.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _main_container.add_child(button_desc_box) + button_desc_box.add_child(button_box) + button_desc_box.add_child(button_desc) + + _btn_in_editor = _add_run_mode_button("In Editor (default)", button_desc, + "This is the default. Runs through the editor. When an error occurs " + + "the debugger is invoked. [b]print[/b] output " + + "appears in the Output panel and errors show up in the Debugger panel.") + button_box.add_child(_btn_in_editor) + _btn_blocking = _add_run_mode_button("Externally - Blocking", button_desc, + "Debugger is not enabled, and cannot be enabled. All output (print, errors, warnings, etc) " + + "appears in the GUT panel, and [b]not[/b] the Output or Debugger panels. \n" + + "The Editor cannot be used while tests are running. If you are trying to test for errors, this " + + "mode provides the best output.") + button_box.add_child(_btn_blocking) + _btn_non_blocking = _add_run_mode_button("Externally - NonBlocking", button_desc, + "Debugger is not enabled, and cannot be enabled. All output (print, errors, warnings, etc) " + + "appears in the GUT panel, and [b]not[/b] the Output or Debugger panels. \n" + + "Test output is streamed to the GUT panel. The editor is not blocked, but can be less " + + "responsive when there is a lot of output. This is the only mode that supports the --headless argument." ) + button_box.add_child(_btn_non_blocking) + + _add_title("Command Line Arguments") + _arg_vbox = VBoxContainer.new() + _main_container.add_child(_arg_vbox) + opt_maker.base_container = _arg_vbox + _txt_additional_arguments = opt_maker.add_value("additional_arguments", additional_arguments, '', '') + _txt_additional_arguments.value_ctrl.placeholder_text = "Put your arguments here. Ex: --verbose -glog 0" + _txt_additional_arguments.value_ctrl.select_all_on_focus = false + _add_blurb( + "Supply any command line options for GUT and/or Godot when running externally. You cannot use " + + "spaces in values. See the Godot and GUT documentation for valid arguments. GUT arguments " + + "specified here take precedence over your config.") + _add_blurb("[b]Be Careful[/b] There are plenty of argument combinations that may make this " + + "act wrong/odd/bad/horrible. Some arguments you might [i]want[/i] " + + "to use but [b]shouldn't[/b] are checked for, but not that many. Choose your arguments carefully (generally good advice).") + + opt_maker.base_container = _main_container + _add_title("Display CLI Help") + _add_blurb("You can use these buttons to get a list of valid GUT and Godot options. They print the CLI help text for each to the [b]Output Panel[/b].") + _btn_godot_help = Button.new() + _btn_godot_help.text = "Print Godot CLI Help" + _main_container.add_child(_btn_godot_help) + _btn_godot_help.pressed.connect(func(): + await _show_help("get_godot_help")) + + _btn_gut_help = Button.new() + _btn_gut_help.text = "Print GUT CLI Help" + _main_container.add_child(_btn_gut_help) + _btn_gut_help.pressed.connect(func(): + await _show_help("get_gut_help")) + + _opt_maker_setup = true + + +func _show_help(help_method_name): + _btn_godot_help.disabled = true + _btn_gut_help.disabled = true + var re = GutUtils.RunExternallyScene.instantiate() + add_child(re) + re.visible = false + var text = await re.call(help_method_name) + print(text) + re.queue_free() + _btn_godot_help.disabled = false + _btn_gut_help.disabled = false + if(GutEditorGlobals.gut_plugin != null): + GutEditorGlobals.gut_plugin.show_output_panel() + + +func _save_to_config_file(f : ConfigFile): + f.clear() + f.set_value('main', 'run_mode', run_mode) + f.set_value('main', 'additional_arguments', opt_maker.controls.additional_arguments.value) + + +func save_to_file(path = default_path): + _save_to_config_file(_config_file) + _config_file.save(path) + + +func _load_from_config_file(f): + run_mode = f.get_value('main', 'run_mode', RUN_MODE_EDITOR) + opt_maker.controls.additional_arguments.value = \ + f.get_value('main', 'additional_arguments', '') + + +func load_from_file(path = default_path): + _config_file.load(path) + _load_from_config_file(_config_file) + + +func reset(): + _load_from_config_file(_config_file) + + +func get_additional_arguments_array(): + return additional_arguments.split(" ", false) + + +func should_run_externally(): + return run_mode != RUN_MODE_EDITOR + + +var _invalid_args = [ + '-d', '--debug', + '-s', '--script', + '-e', '--editor' +] +var _invalid_blocking_args = [ + '--headless' +] +func validate_arguments(): + var arg_array = get_additional_arguments_array() + var i = 0 + var invalid_found = false + while i < _invalid_args.size() and !invalid_found: + if(arg_array.has(_invalid_args[i])): + invalid_found = true + i += 1 + + if(run_mode == RUN_MODE_BLOCKING): + i = 0 + while i < _invalid_blocking_args.size() and !invalid_found: + if(arg_array.has(_invalid_blocking_args[i])): + invalid_found = true + i += 1 + + return !invalid_found + + +func get_godot_help(): + return '' diff --git a/addons/gut/gui/ShellOutOptions.gd.uid b/addons/gut/gui/ShellOutOptions.gd.uid new file mode 100644 index 0000000..22c90ec --- /dev/null +++ b/addons/gut/gui/ShellOutOptions.gd.uid @@ -0,0 +1 @@ +uid://c64u22kybimgi diff --git a/addons/gut/gui/ShellOutOptions.tscn b/addons/gut/gui/ShellOutOptions.tscn new file mode 100644 index 0000000..72442a5 --- /dev/null +++ b/addons/gut/gui/ShellOutOptions.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=2 format=3 uid="uid://ckv5eh8xyrwbk"] + +[ext_resource type="Script" uid="uid://c64u22kybimgi" path="res://addons/gut/gui/ShellOutOptions.gd" id="1_ht2pf"] + +[node name="ShellOutOptions" type="ConfirmationDialog"] +oversampling_override = 1.0 +title = "GUT Run Mode (Experimental)" +position = Vector2i(0, 36) +size = Vector2i(516, 557) +visible = true +script = ExtResource("1_ht2pf") + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +custom_minimum_size = Vector2(500, 500) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="AcceptDialog" type="AcceptDialog" parent="."] +oversampling_override = 1.0 +size = Vector2i(399, 106) +dialog_text = "Invalid arguments. The following cannot be used: +-d --debug -s --script" diff --git a/addons/gut/gui/ShortcutButton.gd b/addons/gut/gui/ShortcutButton.gd new file mode 100644 index 0000000..f6afafc --- /dev/null +++ b/addons/gut/gui/ShortcutButton.gd @@ -0,0 +1,154 @@ +@tool +extends Control + + +@onready var _ctrls = { + shortcut_label = $Layout/lblShortcut, + set_button = $Layout/SetButton, + save_button = $Layout/SaveButton, + cancel_button = $Layout/CancelButton, + clear_button = $Layout/ClearButton +} + +signal changed +signal start_edit +signal end_edit + +const NO_SHORTCUT = '' + +var _source_event = InputEventKey.new() +var _pre_edit_event = null +var _key_disp = NO_SHORTCUT +var _editing = false + +var _modifier_keys = [KEY_ALT, KEY_CTRL, KEY_META, KEY_SHIFT] + +# Called when the node enters the scene tree for the first time. +func _ready(): + set_process_unhandled_key_input(false) + + +func _display_shortcut(): + if(_key_disp == ''): + _key_disp = NO_SHORTCUT + _ctrls.shortcut_label.text = _key_disp + + +func _is_shift_only_modifier(): + return _source_event.shift_pressed and \ + !(_source_event.alt_pressed or \ + _source_event.ctrl_pressed or \ + _source_event.meta_pressed) \ + and !_is_modifier(_source_event.keycode) + + +func _has_modifier(event): + return event.alt_pressed or event.ctrl_pressed or \ + event.meta_pressed or event.shift_pressed + + +func _is_modifier(keycode): + return _modifier_keys.has(keycode) + + +func _edit_mode(should): + _editing = should + set_process_unhandled_key_input(should) + _ctrls.set_button.visible = !should + _ctrls.save_button.visible = should + _ctrls.save_button.disabled = should + _ctrls.cancel_button.visible = should + _ctrls.clear_button.visible = !should + + if(should and to_s() == ''): + _ctrls.shortcut_label.text = 'press buttons' + else: + _ctrls.shortcut_label.text = to_s() + + if(should): + emit_signal("start_edit") + else: + emit_signal("end_edit") + +# --------------- +# Events +# --------------- +func _unhandled_key_input(event): + if(event is InputEventKey): + if(event.pressed): + if(_has_modifier(event) and !_is_modifier(event.get_keycode_with_modifiers())): + _source_event = event + _key_disp = OS.get_keycode_string(event.get_keycode_with_modifiers()) + else: + _source_event = InputEventKey.new() + _key_disp = NO_SHORTCUT + _display_shortcut() + _ctrls.save_button.disabled = !is_valid() + + +func _on_SetButton_pressed(): + _pre_edit_event = _source_event.duplicate(true) + _edit_mode(true) + + +func _on_SaveButton_pressed(): + _edit_mode(false) + _pre_edit_event = null + emit_signal('changed') + + +func _on_CancelButton_pressed(): + cancel() + + +func _on_ClearButton_pressed(): + clear_shortcut() + +# --------------- +# Public +# --------------- +func to_s(): + return OS.get_keycode_string(_source_event.get_keycode_with_modifiers()) + + +func is_valid(): + return _has_modifier(_source_event) and !_is_shift_only_modifier() + + +func get_shortcut(): + var to_return = Shortcut.new() + to_return.events.append(_source_event) + return to_return + +func get_input_event(): + return _source_event + +func set_shortcut(sc): + if(sc == null or sc.events == null or sc.events.size() <= 0): + clear_shortcut() + else: + _source_event = sc.events[0] + _key_disp = to_s() + _display_shortcut() + + +func clear_shortcut(): + _source_event = InputEventKey.new() + _key_disp = NO_SHORTCUT + _display_shortcut() + + +func disable_set(should): + _ctrls.set_button.disabled = should + + +func disable_clear(should): + _ctrls.clear_button.disabled = should + + +func cancel(): + if(_editing): + _edit_mode(false) + _source_event = _pre_edit_event + _key_disp = to_s() + _display_shortcut() diff --git a/addons/gut/gui/ShortcutButton.gd.uid b/addons/gut/gui/ShortcutButton.gd.uid new file mode 100644 index 0000000..1e756d8 --- /dev/null +++ b/addons/gut/gui/ShortcutButton.gd.uid @@ -0,0 +1 @@ +uid://k6hvvpekp0xw diff --git a/addons/gut/gui/ShortcutButton.tscn b/addons/gut/gui/ShortcutButton.tscn new file mode 100644 index 0000000..896854a --- /dev/null +++ b/addons/gut/gui/ShortcutButton.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=2 format=3 uid="uid://sfb1fw8j6ufu"] + +[ext_resource type="Script" uid="uid://k6hvvpekp0xw" path="res://addons/gut/gui/ShortcutButton.gd" id="1"] + +[node name="ShortcutButton" type="Control"] +custom_minimum_size = Vector2(210, 30) +layout_mode = 3 +anchor_right = 0.123 +anchor_bottom = 0.04 +offset_right = 68.304 +offset_bottom = 6.08 +script = ExtResource("1") + +[node name="Layout" type="HBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="lblShortcut" type="Label" parent="Layout"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 7 +text = "" +horizontal_alignment = 2 + +[node name="CenterContainer" type="CenterContainer" parent="Layout"] +custom_minimum_size = Vector2(10, 0) +layout_mode = 2 + +[node name="SetButton" type="Button" parent="Layout"] +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Set" + +[node name="SaveButton" type="Button" parent="Layout"] +visible = false +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Save" + +[node name="CancelButton" type="Button" parent="Layout"] +visible = false +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Cancel" + +[node name="ClearButton" type="Button" parent="Layout"] +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Clear" + +[connection signal="pressed" from="Layout/SetButton" to="." method="_on_SetButton_pressed"] +[connection signal="pressed" from="Layout/SaveButton" to="." method="_on_SaveButton_pressed"] +[connection signal="pressed" from="Layout/CancelButton" to="." method="_on_CancelButton_pressed"] +[connection signal="pressed" from="Layout/ClearButton" to="." method="_on_ClearButton_pressed"] diff --git a/addons/gut/gui/ShortcutDialog.gd b/addons/gut/gui/ShortcutDialog.gd new file mode 100644 index 0000000..283de1d --- /dev/null +++ b/addons/gut/gui/ShortcutDialog.gd @@ -0,0 +1,120 @@ +@tool +extends ConfirmationDialog + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') +var default_path = GutEditorGlobals.editor_shortcuts_path + + +@onready var scbtn_run_all = $Scroll/Layout/CRunAll/ShortcutButton +@onready var scbtn_run_current_script = $Scroll/Layout/CRunCurrentScript/ShortcutButton +@onready var scbtn_run_current_inner = $Scroll/Layout/CRunCurrentInner/ShortcutButton +@onready var scbtn_run_current_test = $Scroll/Layout/CRunCurrentTest/ShortcutButton +@onready var scbtn_run_at_cursor = $Scroll/Layout/CRunAtCursor/ShortcutButton +@onready var scbtn_rerun = $Scroll/Layout/CRerun/ShortcutButton +@onready var scbtn_panel = $Scroll/Layout/CPanelButton/ShortcutButton +@onready var scbtn_windowed = $Scroll/Layout/CToggleWindowed/ShortcutButton + + +@onready var all_buttons = [ + scbtn_run_all, scbtn_run_current_script, scbtn_run_current_inner, + scbtn_run_current_test, scbtn_run_at_cursor, scbtn_rerun, + scbtn_panel, scbtn_windowed +] + + +func _debug_ready(): + popup_centered() + + var btn = Button.new() + btn.text = "show" + get_tree().root.add_child(btn) + btn.pressed.connect(popup) + btn.position = Vector2(100, 100) + btn.size = Vector2(100, 100) + + size_changed.connect(func(): title = str(size)) + + +func _ready(): + for scbtn in all_buttons: + scbtn.connect('start_edit', _on_edit_start.bind(scbtn)) + scbtn.connect('end_edit', _on_edit_end) + + canceled.connect(_on_cancel) + + # Sizing this window on different monitors, especially compared to what it + # looks like if you just run this project is annoying. This is what I came + # up with after getting annoyed. You probably won't be looking at this + # very often so it's fine...until it isn't. + size = Vector2(DisplayServer.screen_get_size()) * Vector2(.5, .8) + + if(get_parent() == get_tree().root): + _debug_ready.call_deferred() + + + +func _cancel_all(): + for scbtn in all_buttons: + scbtn.cancel() + + +# ------------ +# Events +# ------------ +func _on_cancel(): + _cancel_all() + load_shortcuts() + + +func _on_edit_start(which): + for scbtn in all_buttons: + if(scbtn != which): + scbtn.disable_set(true) + scbtn.disable_clear(true) + + +func _on_edit_end(): + for scbtn in all_buttons: + scbtn.disable_set(false) + scbtn.disable_clear(false) + + +# ------------ +# Public +# ------------ +func save_shortcuts(): + save_shortcuts_to_file(default_path) + + +func save_shortcuts_to_file(path): + var f = ConfigFile.new() + f.set_value('main', 'panel_button', scbtn_panel.get_shortcut()) + f.set_value('main', 'rerun', scbtn_rerun.get_shortcut()) + f.set_value('main', 'run_all', scbtn_run_all.get_shortcut()) + f.set_value('main', 'run_at_cursor', scbtn_run_at_cursor.get_shortcut()) + f.set_value('main', 'run_current_inner', scbtn_run_current_inner.get_shortcut()) + f.set_value('main', 'run_current_script', scbtn_run_current_script.get_shortcut()) + f.set_value('main', 'run_current_test', scbtn_run_current_test.get_shortcut()) + # f.set_value('main', 'toggle_windowed', scbtn_windowed.get_shortcut()) + f.save(path) + + +func load_shortcuts(): + load_shortcuts_from_file(default_path) + + +func load_shortcuts_from_file(path): + var f = ConfigFile.new() + # as long as this shortcut is never modified, this is fine, otherwise + # each thing should get its own default instead. + var empty = Shortcut.new() + + f.load(path) + scbtn_panel.set_shortcut(f.get_value('main', 'panel_button', empty)) + scbtn_rerun.set_shortcut(f.get_value('main', 'rerun', empty)) + scbtn_run_all.set_shortcut(f.get_value('main', 'run_all', empty)) + scbtn_run_at_cursor.set_shortcut(f.get_value('main', 'run_at_cursor', empty)) + scbtn_run_current_inner.set_shortcut(f.get_value('main', 'run_current_inner', empty)) + scbtn_run_current_script.set_shortcut(f.get_value('main', 'run_current_script', empty)) + scbtn_run_current_test.set_shortcut(f.get_value('main', 'run_current_test', empty)) + # scbtn_windowed.set_shortcut(f.get_value('main', 'toggle_windowed', empty)) diff --git a/addons/gut/gui/ShortcutDialog.gd.uid b/addons/gut/gui/ShortcutDialog.gd.uid new file mode 100644 index 0000000..7883ae0 --- /dev/null +++ b/addons/gut/gui/ShortcutDialog.gd.uid @@ -0,0 +1 @@ +uid://dc5jgemxslgvl diff --git a/addons/gut/gui/ShortcutDialog.tscn b/addons/gut/gui/ShortcutDialog.tscn new file mode 100644 index 0000000..546d4e1 --- /dev/null +++ b/addons/gut/gui/ShortcutDialog.tscn @@ -0,0 +1,209 @@ +[gd_scene format=3 uid="uid://dj5ve0bq7xa5j"] + +[ext_resource type="Script" uid="uid://dc5jgemxslgvl" path="res://addons/gut/gui/ShortcutDialog.gd" id="1_qq8qn"] +[ext_resource type="PackedScene" uid="uid://sfb1fw8j6ufu" path="res://addons/gut/gui/ShortcutButton.tscn" id="2_i3wie"] + +[node name="ShortcutDialog" type="ConfirmationDialog" unique_id=1776753623] +oversampling_override = 1.0 +title = "GUT Shortcuts" +position = Vector2i(0, 36) +size = Vector2i(1920, 1728) +visible = true +script = ExtResource("1_qq8qn") + +[node name="Scroll" type="ScrollContainer" parent="." unique_id=1914806303] +offset_left = 8.0 +offset_top = 8.0 +offset_right = 1912.0 +offset_bottom = 1679.0 + +[node name="Layout" type="VBoxContainer" parent="Scroll" unique_id=501181924] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="ShortcutDescription" type="RichTextLabel" parent="Scroll/Layout" unique_id=945401700] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "Shortcuts for: + - Buttons in Panel + - Project->Tools->GUT menu items +Shortcuts that only apply to menus are labeled." +fit_content = true +scroll_active = false + +[node name="TopPad" type="CenterContainer" parent="Scroll/Layout" unique_id=1405248155] +custom_minimum_size = Vector2(0, 5) +layout_mode = 2 + +[node name="CPanelButton" type="HBoxContainer" parent="Scroll/Layout" unique_id=1970478925] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CPanelButton" unique_id=1534644867] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Show/Hide GUT" + +[node name="ShortcutButton" parent="Scroll/Layout/CPanelButton" unique_id=2131329924 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription2" type="RichTextLabel" parent="Scroll/Layout" unique_id=2145896902] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Show/hide the gut panel or move focus to/away from the GUT window. +[/i]" +fit_content = true +scroll_active = false + +[node name="CRunAll" type="HBoxContainer" parent="Scroll/Layout" unique_id=1158078093] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRunAll" unique_id=1066237403] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Run All" + +[node name="ShortcutButton" parent="Scroll/Layout/CRunAll" unique_id=738525032 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription3" type="RichTextLabel" parent="Scroll/Layout" unique_id=140757094] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Run the entire test suite.[/i]" +fit_content = true +scroll_active = false + +[node name="CRunCurrentScript" type="HBoxContainer" parent="Scroll/Layout" unique_id=736016343] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentScript" unique_id=950095716] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Run Current Script" + +[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentScript" unique_id=983079158 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription4" type="RichTextLabel" parent="Scroll/Layout" unique_id=1806489359] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Run all tests in the currently selected script.[/i]" +fit_content = true +scroll_active = false + +[node name="CRunCurrentInner" type="HBoxContainer" parent="Scroll/Layout" unique_id=2095658353] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentInner" unique_id=1492835813] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Run Current Inner Class" + +[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentInner" unique_id=1400542413 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription5" type="RichTextLabel" parent="Scroll/Layout" unique_id=1669546632] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Run only the currently selected inner test class if one is selected.[/i]" +fit_content = true +scroll_active = false + +[node name="CRunCurrentTest" type="HBoxContainer" parent="Scroll/Layout" unique_id=641427686] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentTest" unique_id=879088006] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Run Current Test" + +[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentTest" unique_id=1073304800 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription6" type="RichTextLabel" parent="Scroll/Layout" unique_id=164300543] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Run only the currently selected test, if one is selected[/i]" +fit_content = true +scroll_active = false + +[node name="CRunAtCursor" type="HBoxContainer" parent="Scroll/Layout" unique_id=1201163993] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRunAtCursor" unique_id=1748949401] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Run At Cursor (menu only)" + +[node name="ShortcutButton" parent="Scroll/Layout/CRunAtCursor" unique_id=282512521 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription7" type="RichTextLabel" parent="Scroll/Layout" unique_id=1040671512] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Run the most specific test/inner class/script based on where the cursor is.[/i]" +fit_content = true +scroll_active = false + +[node name="CRerun" type="HBoxContainer" parent="Scroll/Layout" unique_id=1620841467] +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CRerun" unique_id=1644131487] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Rerun (menu only)" + +[node name="ShortcutButton" parent="Scroll/Layout/CRerun" unique_id=782836241 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription8" type="RichTextLabel" parent="Scroll/Layout" unique_id=520952349] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Rerun the test(s) that were last run." +fit_content = true +scroll_active = false + +[node name="CToggleWindowed" type="HBoxContainer" parent="Scroll/Layout" unique_id=1521839247] +visible = false +layout_mode = 2 + +[node name="Label" type="Label" parent="Scroll/Layout/CToggleWindowed" unique_id=728639840] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +size_flags_vertical = 7 +text = "Toggle Windowed" + +[node name="ShortcutButton" parent="Scroll/Layout/CToggleWindowed" unique_id=603114021 instance=ExtResource("2_i3wie")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ShortcutDescription9" type="RichTextLabel" parent="Scroll/Layout" unique_id=747035069] +visible = false +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +bbcode_enabled = true +text = "[i]Toggle GUT in the bottom panel or a separate window.[/i]" +fit_content = true +scroll_active = false diff --git a/addons/gut/gui/about.gd b/addons/gut/gui/about.gd new file mode 100644 index 0000000..b982207 --- /dev/null +++ b/addons/gut/gui/about.gd @@ -0,0 +1,136 @@ +@tool +extends AcceptDialog + +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + +var _bbcode = \ +""" +[center][b]GUT Links[/b] +{gut_link_table}[/center] + +[center][b]VSCode Extension Links[/b] +{vscode_link_table}[/center] + +[center]You can support GUT development at: +{donate_link} + +Thanks for using GUT! +[/center] +""" + +var _gut_links = [ + [&"Documentation", &"https://gut.readthedocs.io"], + [&"What's New", &"https://github.com/bitwes/Gut/releases/tag/v{gut_version}"], + [&"Repo", &"https://github.com/bitwes/gut"], + [&"Report Bugs", &"https://github.com/bitwes/gut/issues"] +] + +var _vscode_links = [ + ["Repo", "https://github.com/bitwes/gut-extension"], + ["Market Place", "https://marketplace.visualstudio.com/items?itemName=bitwes.gut-extension"] +] + +var _donate_link = "https://buymeacoffee.com/bitwes" +@onready var _logo = $Logo +@onready var rtl = $HBox/VBoxContainer/Scroll/RichTextLabel +@onready var check_for_update = $HBox/VBoxContainer/CheckForUpdate + +func _ready(): + if(get_parent() is SubViewport): + return + elif(get_parent() == get_tree().root): + size = Vector2(100, 100) + title = str("GUT ", GutUtils.version_numbers.gut_version) + _vert_center_logo() + _logo.disabled = true + rtl.text = _make_text() + + +func _color_link(link_text): + return str("[color=ROYAL_BLUE]", link_text, "[/color]") + + +func _link_table(entries): + var text = '' + for entry in entries: + text += str("[cell][right]", entry[0], "[/right][/cell]") + var link = str("[url]", entry[1], "[/url]") + if(entry[1].length() > 60): + link = str("[url=", entry[1], "]", entry[1].substr(0, 50), "...[/url]") + + text += str("[cell][left]", _color_link(link), "[/left][/cell]\n") + return str('[table=2]', text, '[/table]') + + +func url_bbcode(url, link_text=null): + if(link_text == null): + link_text = url + var text = str("[url=", url, "]", link_text, "[/url]") + return _color_link(text) + + +func _make_text(): + var gut_link_table = _link_table(_gut_links) + var vscode_link_table = _link_table(_vscode_links) + + var text = _bbcode.format({ + "gut_link_table":gut_link_table, + "vscode_link_table":vscode_link_table, + "donate_link":_color_link(str('[url]', _donate_link, '[/url]')), + }) + return text + + +func _vert_center_logo(): + _logo.position.y = size.y / 2.0 + + +# ----------- +# Events +# ----------- +func _on_rich_text_label_meta_clicked(meta: Variant) -> void: + OS.shell_open(str(meta)) + + +func _on_mouse_entered() -> void: + pass#_logo.active = true + + +func _on_mouse_exited() -> void: + pass#_logo.active = false + + +var _odd_ball_eyes_l = 1.1 +var _odd_ball_eyes_r = .7 +func _on_rich_text_label_meta_hover_started(meta: Variant) -> void: + if(meta == _gut_links[0][1]): + _logo.set_eye_color(Color.RED) + elif(meta.find("releases/tag/") > 0): + _logo.set_eye_color(Color.GREEN) + elif(meta == _gut_links[2][1]): + _logo.set_eye_color(Color.PURPLE) + elif(meta == _gut_links[3][1]): + _logo.set_eye_scale(1.2) + elif(meta == _vscode_links[0][1]): + _logo.set_eye_scale(.5, .5) + elif(meta == _vscode_links[1][1]): + _logo.set_eye_scale(_odd_ball_eyes_l, _odd_ball_eyes_r) + var temp = _odd_ball_eyes_l + _odd_ball_eyes_l = _odd_ball_eyes_r + _odd_ball_eyes_r = temp + elif(meta == _donate_link): + _logo.active = false + + +func _on_rich_text_label_meta_hover_ended(meta: Variant) -> void: + if(meta == _donate_link): + _logo.active = true + + +func _on_logo_pressed() -> void: + _logo.disabled = !_logo.disabled + + +func _on_check_for_update_verbose_enabled() -> void: + check_for_update.size_flags_vertical = check_for_update.SIZE_EXPAND_FILL + check_for_update.z_index = 100 diff --git a/addons/gut/gui/about.gd.uid b/addons/gut/gui/about.gd.uid new file mode 100644 index 0000000..beeb0f8 --- /dev/null +++ b/addons/gut/gui/about.gd.uid @@ -0,0 +1 @@ +uid://g7qu8ihdt3pd diff --git a/addons/gut/gui/about.tscn b/addons/gut/gui/about.tscn new file mode 100644 index 0000000..bddb852 --- /dev/null +++ b/addons/gut/gui/about.tscn @@ -0,0 +1,68 @@ +[gd_scene format=3 uid="uid://dqbkylpsatcqm"] + +[ext_resource type="Script" uid="uid://g7qu8ihdt3pd" path="res://addons/gut/gui/about.gd" id="1_bg86c"] +[ext_resource type="PackedScene" uid="uid://c5om15m2e70qp" path="res://addons/gut/gui/check_for_update.tscn" id="2_kpic4"] +[ext_resource type="PackedScene" uid="uid://bjkn8mhx2fmt1" path="res://addons/gut/gui/GutLogo.tscn" id="3_kpic4"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q8rky"] +bg_color = Color(0, 0, 0, 0.49803922) + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kpic4"] + +[node name="About" type="AcceptDialog" unique_id=1150745302] +oversampling_override = 1.0 +title = "About GUT" +position = Vector2i(0, 36) +size = Vector2i(1500, 800) +visible = true +min_size = Vector2i(800, 800) +script = ExtResource("1_bg86c") + +[node name="HBox" type="HBoxContainer" parent="." unique_id=1875890740] +offset_left = 8.0 +offset_top = 8.0 +offset_right = 1492.0 +offset_bottom = 751.0 +alignment = 1 + +[node name="MakeRoomForLogo" type="CenterContainer" parent="HBox" unique_id=1274652102] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="HBox" unique_id=1594624944] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 0 + +[node name="CheckForUpdate" parent="HBox/VBoxContainer" unique_id=381385621 instance=ExtResource("2_kpic4")] +custom_minimum_size = Vector2(0, 76) +layout_mode = 2 + +[node name="Scroll" type="ScrollContainer" parent="HBox/VBoxContainer" unique_id=1684204946] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="RichTextLabel" type="RichTextLabel" parent="HBox/VBoxContainer/Scroll" unique_id=1217183710] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_styles/normal = SubResource("StyleBoxFlat_q8rky") +theme_override_styles/focus = SubResource("StyleBoxEmpty_kpic4") +bbcode_enabled = true +fit_content = true + +[node name="Logo" parent="." unique_id=2097718136 instance=ExtResource("3_kpic4")] +modulate = Color(0.74509805, 0.74509805, 0.74509805, 1) +position = Vector2(151, 265) +scale = Vector2(0.8, 0.8) +active = true +disabled = true + +[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] +[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] +[connection signal="verbose_enabled" from="HBox/VBoxContainer/CheckForUpdate" to="." method="_on_check_for_update_verbose_enabled"] +[connection signal="meta_clicked" from="HBox/VBoxContainer/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"] +[connection signal="meta_hover_ended" from="HBox/VBoxContainer/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_ended"] +[connection signal="meta_hover_started" from="HBox/VBoxContainer/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_started"] +[connection signal="pressed" from="Logo" to="." method="_on_logo_pressed"] diff --git a/addons/gut/gui/arrow.png b/addons/gut/gui/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..d407714ccdeb1e30c6e363aab27146d3f9a03e97 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe4i*Lm2K9!kZ43+yj-D=#Ar-fJPAM`TaNuFs zXeB@Qbq%XXOu@59^V=dir-G2>s>^|$QzK08A2AcYy>{!>TUohVw?##-)y?Ofk;G7t X void: + if(_mouse_down_duration < _mouse_down_time_to_show_verbose and _mouse_down): + _mouse_down_duration += delta + if(_mouse_down_duration >= _mouse_down_time_to_show_verbose): + _verbose = true + rtl.append_text("\nVERBOSE ENABLED\n") + rtl.append_text(_get_check_for_update_link()) + + +# ---------------- +# Private +# ---------------- + +func _log(text): + if(_verbose): + _log_entries.append(str(text)) + + +func _log_file(path): + if(_verbose): + var file_text = FileAccess.get_file_as_string(path) + if(file_text == ""): + file_text = "--Missing or empty file--" + _log(str(path, ":\n[code]", file_text, "[/code]")) + + +func _get_check_for_update_link(): + if(_verbose or update_detector.fetch_limit_wait_time() <= 0.0): + return str("[center]", _url_bbcode("_check_for_update", "Check for Update", "ORANGE"), "[/center]") + else: + return '' + + +func check_for_update(use_fetch, force=false): + _log_entries.clear() + rtl.text = "" + update_detector.check_for_update() + if(use_fetch): + fetch_count += 1 + rtl.text = "Checking..." + _log("fetching remote file " + update_detector.REMOTE_FILE_URL) + update_detector.check_for_update_with_fetch(force) + else: + _log("local check") + update_detector.check_for_update() + + +func _populate_text(): + var txt = "" + if(!update_detector.is_empty()): + txt = str("[center]", update_detector.get_update_string(_url_bbcode), "[/center]") + if(_verbose): + txt = txt + "\n\n" + "\n".join(_log_entries) + rtl.text = txt + "\n" + _get_check_for_update_link() + _post_populate.call_deferred() + + +func _post_populate(): + custom_minimum_size.y = min(rtl.get_content_height() + 30, 400) + + +func _url_bbcode(url, link_text=null, color_name="ROYAL_BLUE"): + if(link_text == null): + link_text = url + var text = str("[url=", url, "]", link_text, "[/url]") + return str("[color=", color_name, "]", text, "[/color]") + + +# ----------------- +# Events +# ----------------- +func _on_update_detector_updated(): + _log_file(update_detector.REMOTE_FILE_PATH) + _log_file(update_detector.LOCAL_FILE_PATH) + _log(str("Local:\n[code]", JSON.stringify(update_detector.local_data.get_data(), " "), "[/code]")) + _log(str("Local Issues:\n", update_detector.local_data.data_issues)) + _log(str("Remote:\n[code]", JSON.stringify(update_detector.remote_data.get_data(), " "), "[/code]")) + _log(str("Remote Issues:\n", update_detector.remote_data.data_issues)) + + _log(update_detector.get_summary_string()) + _populate_text() + + +func _on_btn_check_button_up() -> void: + check_for_update(true, true) + + +func _on_update_detector_download(): + _log("Download completed") + + +func _on_output_meta_clicked(meta: Variant) -> void: + if(meta == "_check_for_update"): + check_for_update(true, true) + else: + OS.shell_open(str(meta)) + + +func _on_output_gui_input(event: InputEvent) -> void: + if(event is InputEventMouseButton): + if(event.pressed): + _mouse_down = true + else: + _mouse_down = false + _mouse_down_duration = 0.0 + + +func _on_output_resized() -> void: + if(rtl != null): + custom_minimum_size.y = rtl.get_visible_content_rect().size.y + 20 + + +func _on_mouse_exited() -> void: + _mouse_down = false + _mouse_down = 0.0 diff --git a/addons/gut/gui/check_for_update.gd.uid b/addons/gut/gui/check_for_update.gd.uid new file mode 100644 index 0000000..eee7c25 --- /dev/null +++ b/addons/gut/gui/check_for_update.gd.uid @@ -0,0 +1 @@ +uid://bomsutomy3pdy diff --git a/addons/gut/gui/check_for_update.tscn b/addons/gut/gui/check_for_update.tscn new file mode 100644 index 0000000..3b1fca7 --- /dev/null +++ b/addons/gut/gui/check_for_update.tscn @@ -0,0 +1,45 @@ +[gd_scene format=3 uid="uid://c5om15m2e70qp"] + +[ext_resource type="Script" uid="uid://bomsutomy3pdy" path="res://addons/gut/gui/check_for_update.gd" id="1_70x43"] +[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_lsin2"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lsin2"] +content_margin_left = 20.0 +content_margin_top = 20.0 +content_margin_right = 20.0 +content_margin_bottom = 20.0 +bg_color = Color(0.09239707, 0.09239706, 0.09239706, 0.49803922) +corner_radius_top_left = 20 +corner_radius_top_right = 20 + +[node name="CheckForUpdate" type="Control" unique_id=381385621] +custom_minimum_size = Vector2(0, 53) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +script = ExtResource("1_70x43") + +[node name="Output" type="RichTextLabel" parent="." unique_id=1256208807] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +focus_mode = 2 +theme_override_fonts/mono_font = ExtResource("2_lsin2") +theme_override_styles/normal = SubResource("StyleBoxFlat_lsin2") +bbcode_enabled = true +text = "[center][color=ROYAL_BLUE][url=https://github.com/bitwes/Gut/releases/tag/v0.0.0]GUT 0.0.0[/url][/color] is now available![/center] +" +selection_enabled = true + +[connection signal="gui_input" from="Output" to="." method="_on_output_gui_input"] +[connection signal="meta_clicked" from="Output" to="." method="_on_output_meta_clicked"] +[connection signal="resized" from="Output" to="." method="_on_output_resized"] diff --git a/addons/gut/gui/editor_globals.gd b/addons/gut/gui/editor_globals.gd new file mode 100644 index 0000000..da64eb4 --- /dev/null +++ b/addons/gut/gui/editor_globals.gd @@ -0,0 +1,68 @@ +@tool + +static var GutUserPreferences = load("res://addons/gut/gui/gut_user_preferences.gd") +static var temp_directory = 'user://gut_temp_directory/' + +static var editor_run_gut_config_path = 'gut_editor_config.json': + # This avoids having to use path_join wherever we want to reference this + # path. The value is not supposed to change. Could it be a constant + # instead? Probably, but I didn't like repeating the directory part. + # Do I like that this is a bit witty. Absolutely. + get: return temp_directory.path_join(editor_run_gut_config_path) + # Should this print a message or something instead? Probably, but then I'd + # be repeating even more code than if this was just a constant. So I didn't, + # even though I wanted to make the message a easter eggish fun message. + # I didn't, so this dumb comment will have to serve as the easter eggish fun. + set(v): + print("Be sure to document your code. Never trust comments.") + + +static var editor_run_bbcode_results_path = 'gut_editor.bbcode': + get: return temp_directory.path_join(editor_run_bbcode_results_path) + set(v): pass + + +static var editor_run_json_results_path = 'gut_editor.json': + get: return temp_directory.path_join(editor_run_json_results_path) + set(v): pass + + +static var editor_shortcuts_path = 'gut_editor_shortcuts.cfg' : + get: return temp_directory.path_join(editor_shortcuts_path) + set(v): pass + +static var run_externally_options_path = 'gut_editor_run_externally.cfg' : + get: return temp_directory.path_join(run_externally_options_path) + set(v): pass + +static var _user_prefs = null +static var user_prefs = _user_prefs : + # workaround not being able to reference EditorInterface when not in + # the editor. This shouldn't be referenced by anything not in the + # editor. + get: + if(_user_prefs == null and Engine.is_editor_hint()): + # This is sometimes used when not in the editor. Avoid parser error + # for EditorInterface. + _user_prefs = GutUserPreferences.new(GutUtils.get_editor_interface().get_editor_settings()) + return _user_prefs +static var gut_plugin = null + +static func create_temp_directory(): + DirAccess.make_dir_recursive_absolute(temp_directory) + + +static func is_being_edited_in_editor(which): + if(!Engine.is_editor_hint()): + return false + + var trav = which + var is_scene_root = false + var editor_root = which.get_tree().edited_scene_root + while(trav != null and !is_scene_root): + is_scene_root = editor_root == trav + if(!is_scene_root): + trav = trav.get_parent() + return is_scene_root + + diff --git a/addons/gut/gui/editor_globals.gd.uid b/addons/gut/gui/editor_globals.gd.uid new file mode 100644 index 0000000..c3ae213 --- /dev/null +++ b/addons/gut/gui/editor_globals.gd.uid @@ -0,0 +1 @@ +uid://cbi00ubn046c2 diff --git a/addons/gut/gui/gut_config_gui.gd b/addons/gut/gui/gut_config_gui.gd new file mode 100644 index 0000000..52e3867 --- /dev/null +++ b/addons/gut/gui/gut_config_gui.gd @@ -0,0 +1,282 @@ +var PanelControls = load("res://addons/gut/gui/panel_controls.gd") +var GutConfig = load('res://addons/gut/gut_config.gd') + +const DIRS_TO_LIST = 6 + +# specific titles that we need to do stuff with +var _titles = { + dirs = null +} + +var _cfg_ctrls = {} +var opt_maker = null + +func _init(cont): + opt_maker = GutUtils.OptionMaker.new(cont) + _cfg_ctrls = opt_maker.controls + # _base_container = cont + + +func _add_save_load(): + var ctrl = PanelControls.GpcSaveLoad.new('Config', '', '') + + ctrl.save_path_chosen.connect(_on_save_path_chosen) + ctrl.load_path_chosen.connect(_on_load_path_chosen) + + #_cfg_ctrls['save_load'] = ctrl + opt_maker.add_ctrl('save_load', ctrl) + return ctrl + +# ------------------ +# Events +# ------------------ +func _on_save_path_chosen(path): + save_file(path) + + +func _on_load_path_chosen(path): + load_file.bind(path).call_deferred() + +# ------------------ +# Public +# ------------------ +func get_config_issues(): + var to_return = [] + var has_directory = false + + for i in range(DIRS_TO_LIST): + var key = str('directory_', i) + var path = _cfg_ctrls[key].value + if(path != null and path != ''): + has_directory = true + if(!DirAccess.dir_exists_absolute(path)): + _cfg_ctrls[key].mark_invalid(true) + to_return.append(str('Test directory ', path, ' does not exist.')) + else: + _cfg_ctrls[key].mark_invalid(false) + else: + _cfg_ctrls[key].mark_invalid(false) + + if(!has_directory): + to_return.append('You do not have any directories set.') + _titles.dirs.mark_invalid(true) + else: + _titles.dirs.mark_invalid(false) + + if(!_cfg_ctrls.suffix.value.ends_with('.gd')): + _cfg_ctrls.suffix.mark_invalid(true) + to_return.append("Script suffix must end in '.gd'") + else: + _cfg_ctrls.suffix.mark_invalid(false) + + return to_return + + +func clear(): + opt_maker.clear() + + +func save_file(path): + var gcfg = GutConfig.new() + gcfg.options = get_options({}) + gcfg.save_file(path) + + + +func load_file(path): + var gcfg = GutConfig.new() + gcfg.load_options(path) + clear() + set_options(gcfg.options) + + +# -------------- +# SUPER dumb but VERY fun hack to hide settings. The various _add methods will +# return what they add. If you want to hide it, just assign the result to this. +# YES, I could have just put .visible at the end, but I didn't think of that +# until just now, and this was fun, non-permanent and the .visible at the end +# isn't as obvious as hide_this = +# +# Also, we can't just skip adding the controls because other things are looking +# for them and things start to blow up if you don't add them. +var hide_this = null : + set(val): + val.visible = false + +# -------------- + +func set_options(opts): + var options = opts.duplicate() + + # _add_title('Save/Load') + _add_save_load() + + opt_maker.add_title("Settings") + opt_maker.add_number("log_level", options.log_level, "Log Level", 0, 3, + "Detail level for log messages.\n" + \ + "\t0: Errors and failures only.\n" + \ + "\t1: Adds all test names + warnings + info\n" + \ + "\t2: Shows all asserts\n" + \ + "\t3: Adds more stuff probably, maybe not.") + opt_maker.add_float("wait_log_delay", options.wait_log_delay, "Wait Log Delay", 0.1, 0.0, 999.1, + "How long to wait before displaying 'Awaiting' messages.") + opt_maker.add_boolean('ignore_pause', options.ignore_pause, 'Ignore Pause', + "Ignore calls to pause_before_teardown") + opt_maker.add_boolean('hide_orphans', options.hide_orphans, 'Hide Orphans', + 'Do not display orphan counts in output.') + opt_maker.add_boolean('should_exit', options.should_exit, 'Exit on Finish', + "Exit when tests finished.") + opt_maker.add_boolean('should_exit_on_success', options.should_exit_on_success, 'Exit on Success', + "Exit if there are no failures. Does nothing if 'Exit on Finish' is enabled.") + opt_maker.add_select('double_strategy', 'Script Only', ['Include Native', 'Script Only'], 'Double Strategy', + '"Include Native" will include native methods in Doubles. "Script Only" will not. ' + "\n" + \ + 'The native method override warning is disabled when creating Doubles.' + "\n" + \ + 'This is the default, you can override this at the script level or when creating doubles.') + _cfg_ctrls.double_strategy.value = GutUtils.get_enum_value( + options.double_strategy, GutUtils.DOUBLE_STRATEGY, GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY) + + + opt_maker.add_title("Fail Error Types") + opt_maker.add_boolean("error_tracking", !options.no_error_tracking, 'Track Errors', + "Enable/Disable GUT's ability to detect engine and push errors.") + opt_maker.add_boolean('engine_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_ENGINE), + 'Engine', 'Any script/engine error that occurs during a test will cause the test to fail.') + opt_maker.add_boolean('push_error_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_PUSH_ERROR), + 'Push', 'Any error generated by a call to push_error that occurs during a test will cause the test to fail.') + opt_maker.add_boolean('gut_errors_cause_failure', options.failure_error_types.has(GutConfig.FAIL_ERROR_TYPE_GUT), + 'GUT', 'Any internal GUT error that occurs while a test is running will cause it to fail..') + + + opt_maker.add_title('Runner Appearance') + hide_this = opt_maker.add_boolean("gut_on_top", options.gut_on_top, "On Top", + "The GUT Runner appears above children added during tests.") + opt_maker.add_number('opacity', options.opacity, 'Opacity', 0, 100, + "The opacity of GUT when tests are running.") + hide_this = opt_maker.add_boolean('should_maximize', options.should_maximize, 'Maximize', + "Maximize GUT when tests are being run.") + opt_maker.add_boolean('compact_mode', options.compact_mode, 'Compact Mode', + 'The runner will be in compact mode. This overrides Maximize.') + opt_maker.add_select('font_name', options.font_name, GutUtils.avail_fonts, 'Font', + "The font to use for text output in the Gut Runner.") + opt_maker.add_number('font_size', options.font_size, 'Font Size', 5, 100, + "The font size for text output in the Gut Runner.") + hide_this = opt_maker.add_color('font_color', options.font_color, 'Font Color', + "The font color for text output in the Gut Runner.") + opt_maker.add_color('background_color', options.background_color, 'Background Color', + "The background color for text output in the Gut Runner.") + opt_maker.add_boolean('disable_colors', options.disable_colors, 'Disable Formatting', + 'Disable formatting and colors used in the Runner. Does not affect panel output.') + + + _titles.dirs = opt_maker.add_title('Test Directories') + opt_maker.add_boolean('include_subdirs', options.include_subdirs, 'Include Subdirs', + "Include subdirectories of the directories configured below.") + + var dirs_to_load = options.configured_dirs + if(options.dirs.size() > dirs_to_load.size()): + dirs_to_load = options.dirs + + for i in range(DIRS_TO_LIST): + var value = '' + if(dirs_to_load.size() > i): + value = dirs_to_load[i] + + var test_dir = opt_maker.add_directory(str('directory_', i), value, str(i)) + test_dir.enabled_button.visible = true + test_dir.enabled_button.button_pressed = options.dirs.has(value) + + + opt_maker.add_title("XML Output") + opt_maker.add_save_file_anywhere("junit_xml_file", options.junit_xml_file, "Output Path", + "Path and filename where GUT should create a JUnit compliant XML file. " + + "This file will contain the results of the last test run. To avoid " + + "overriding the file use Include Timestamp.") + opt_maker.add_boolean("junit_xml_timestamp", options.junit_xml_timestamp, "Include Timestamp", + "Include a timestamp in the filename so that each run gets its own xml file.") + + + opt_maker.add_title('Hooks') + opt_maker.add_file('pre_run_script', options.pre_run_script, 'Pre-Run Hook', + 'This script will be run by GUT before any tests are run.') + opt_maker.add_file('post_run_script', options.post_run_script, 'Post-Run Hook', + 'This script will be run by GUT after all tests are run.') + + + opt_maker.add_title('Misc') + opt_maker.add_value('prefix', options.prefix, 'Script Prefix', + "The filename prefix for all test scripts.") + opt_maker.add_value('suffix', options.suffix, 'Script Suffix', + "Script suffix, including .gd extension. For example '_foo.gd'.") + opt_maker.add_float('paint_after', options.paint_after, 'Paint After', .05, 0.0, 1.0, + "How long GUT will wait before pausing for 1 frame to paint the screen. 0 is never.") + + + +func get_options(base_opts): + var to_return = base_opts.duplicate() + + # Settings + to_return.log_level = _cfg_ctrls.log_level.value + to_return.wait_log_delay = _cfg_ctrls.wait_log_delay.value + to_return.ignore_pause = _cfg_ctrls.ignore_pause.value + to_return.hide_orphans = _cfg_ctrls.hide_orphans.value + to_return.should_exit = _cfg_ctrls.should_exit.value + to_return.should_exit_on_success = _cfg_ctrls.should_exit_on_success.value + to_return.double_strategy = _cfg_ctrls.double_strategy.value + + # Runner Appearance + to_return.font_name = _cfg_ctrls.font_name.text + to_return.font_size = _cfg_ctrls.font_size.value + to_return.should_maximize = _cfg_ctrls.should_maximize.value + to_return.compact_mode = _cfg_ctrls.compact_mode.value + to_return.opacity = _cfg_ctrls.opacity.value + to_return.background_color = _cfg_ctrls.background_color.value.to_html() + to_return.font_color = _cfg_ctrls.font_color.value.to_html() + to_return.disable_colors = _cfg_ctrls.disable_colors.value + to_return.gut_on_top = _cfg_ctrls.gut_on_top.value + to_return.paint_after = _cfg_ctrls.paint_after.value + + # Fail Error Types + to_return.no_error_tracking = !_cfg_ctrls.error_tracking + + var fail_error_types = [] + if(_cfg_ctrls.engine_errors_cause_failure.value): + fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_ENGINE) + if(_cfg_ctrls.push_error_errors_cause_failure.value): + fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_PUSH_ERROR) + if(_cfg_ctrls.gut_errors_cause_failure.value): + fail_error_types.append(GutConfig.FAIL_ERROR_TYPE_GUT) + to_return.failure_error_types = fail_error_types + + # Directories + to_return.include_subdirs = _cfg_ctrls.include_subdirs.value + var dirs = [] + var configured_dirs = [] + for i in range(DIRS_TO_LIST): + var key = str('directory_', i) + var ctrl = _cfg_ctrls[key] + if(ctrl.value != '' and ctrl.value != null): + configured_dirs.append(ctrl.value) + if(ctrl.enabled_button.button_pressed): + dirs.append(ctrl.value) + to_return.dirs = dirs + to_return.configured_dirs = configured_dirs + + # XML Output + to_return.junit_xml_file = _cfg_ctrls.junit_xml_file.value + to_return.junit_xml_timestamp = _cfg_ctrls.junit_xml_timestamp.value + + # Hooks + to_return.pre_run_script = _cfg_ctrls.pre_run_script.value + to_return.post_run_script = _cfg_ctrls.post_run_script.value + + # Misc + to_return.prefix = _cfg_ctrls.prefix.value + to_return.suffix = _cfg_ctrls.suffix.value + + return to_return + + +func mark_saved(): + for key in _cfg_ctrls: + _cfg_ctrls[key].mark_unsaved(false) diff --git a/addons/gut/gui/gut_config_gui.gd.uid b/addons/gut/gui/gut_config_gui.gd.uid new file mode 100644 index 0000000..3aa4cd4 --- /dev/null +++ b/addons/gut/gui/gut_config_gui.gd.uid @@ -0,0 +1 @@ +uid://chosc1tvfaduq diff --git a/addons/gut/gui/gut_dock.gd b/addons/gut/gui/gut_dock.gd new file mode 100644 index 0000000..08155d3 --- /dev/null +++ b/addons/gut/gui/gut_dock.gd @@ -0,0 +1,37 @@ +extends EditorDock + + +var _panel : Control = null +var _current_layout = -1 + + +func _update_layout(layout): + _current_layout = layout + if(_panel != null): + if(layout == DOCK_LAYOUT_FLOATING): + _windowed_mode() + else: + _dock_mode() + + +# ------------- +# Private +# ------------- +func _windowed_mode(): + _panel.show_layout_buttons(true) + + +func _dock_mode(): + _panel.results_horiz_layout() + _panel.show_layout_buttons(false) + + +# ------------- +# Public +# ------------- +func add_bottom_panel(gut_bottom_panel): + _panel = gut_bottom_panel + # Make floating button not supported right now + add_child(_panel) + _panel.make_floating_btn.visible = false + _update_layout(_current_layout) diff --git a/addons/gut/gui/gut_dock.gd.uid b/addons/gut/gui/gut_dock.gd.uid new file mode 100644 index 0000000..95a4bcc --- /dev/null +++ b/addons/gut/gui/gut_dock.gd.uid @@ -0,0 +1 @@ +uid://cmj50k82tckur diff --git a/addons/gut/gui/gut_gui.gd b/addons/gut/gui/gut_gui.gd new file mode 100644 index 0000000..618e199 --- /dev/null +++ b/addons/gut/gui/gut_gui.gd @@ -0,0 +1,239 @@ +extends Control +# ############################################################################## +# This is the decoupled GUI for gut.gd +# +# This is a "generic" interface between a GUI and gut.gd. It assumes there are +# certain controls with specific names. It will then interact with those +# controls based on signals emitted from gut.gd in order to give the user +# feedback about the progress of the test run and the results. +# +# Optional controls are marked as such in the _ctrls dictionary. The names +# of the controls can be found in _populate_ctrls. +# ############################################################################## +var _gut = null + +var _ctrls = { + btn_continue = null, + path_dir = null, + path_file = null, + prog_script = null, + prog_test = null, + rtl = null, # optional + rtl_bg = null, # required if rtl exists + switch_modes = null, + time_label = null, + title = null, + title_bar = null, + tgl_word_wrap = null, # optional +} + +var _title_mouse = { + down = false +} + + +signal switch_modes() + +var _max_position = Vector2(100, 100) + +func _ready(): + _populate_ctrls() + + _ctrls.btn_continue.visible = false + _ctrls.btn_continue.pressed.connect(_on_continue_pressed) + _ctrls.switch_modes.pressed.connect(_on_switch_modes_pressed) + _ctrls.title_bar.gui_input.connect(_on_title_bar_input) + if(_ctrls.tgl_word_wrap != null): + _ctrls.tgl_word_wrap.toggled.connect(_on_word_wrap_toggled) + + _ctrls.prog_script.value = 0 + _ctrls.prog_test.value = 0 + _ctrls.path_dir.text = '' + _ctrls.path_file.text = '' + _ctrls.time_label.text = '' + + _max_position = get_display_size() - Vector2(30, _ctrls.title_bar.size.y) + + +func _process(_delta): + if(_gut != null and _gut.is_running()): + set_elapsed_time(_gut.get_elapsed_time()) + + +# ------------------ +# Private +# ------------------ +func get_display_size(): + return get_viewport().get_visible_rect().size + + +func _populate_ctrls(): + # Brute force, but flexible. This allows for all the controls to exist + # anywhere, and as long as they all have the right name, they will be + # found. + _ctrls.btn_continue = _get_first_child_named('Continue', self) + _ctrls.path_dir = _get_first_child_named('Path', self) + _ctrls.path_file = _get_first_child_named('File', self) + _ctrls.prog_script = _get_first_child_named('ProgressScript', self) + _ctrls.prog_test = _get_first_child_named('ProgressTest', self) + _ctrls.rtl = _get_first_child_named('TestOutput', self) + _ctrls.rtl_bg = _get_first_child_named('OutputBG', self) + _ctrls.switch_modes = _get_first_child_named("SwitchModes", self) + _ctrls.time_label = _get_first_child_named('TimeLabel', self) + _ctrls.title = _get_first_child_named("Title", self) + _ctrls.title_bar = _get_first_child_named("TitleBar", self) + _ctrls.tgl_word_wrap = _get_first_child_named("WordWrap", self) + + +func _get_first_child_named(obj_name, parent_obj): + if(parent_obj == null): + return null + + var kids = parent_obj.get_children() + var index = 0 + var to_return = null + + while(index < kids.size() and to_return == null): + if(str(kids[index]).find(str(obj_name, ':')) != -1): + to_return = kids[index] + else: + to_return = _get_first_child_named(obj_name, kids[index]) + if(to_return == null): + index += 1 + + return to_return + + + +# ------------------ +# Events +# ------------------ +func _on_title_bar_input(event : InputEvent): + if(event is InputEventMouseMotion): + if(_title_mouse.down): + position += event.relative + position.x = clamp(position.x, 0, _max_position.x) + position.y = clamp(position.y, 0, _max_position.y) + elif(event is InputEventMouseButton): + if(event.button_index == MOUSE_BUTTON_LEFT): + _title_mouse.down = event.pressed + + +func _on_continue_pressed(): + _gut.end_teardown_pause() + + +func _on_gut_start_run(): + if(_ctrls.rtl != null): + _ctrls.rtl.clear() + set_num_scripts(_gut.get_test_collector().scripts.size()) + + +func _on_gut_end_run(): + _ctrls.prog_test.value = _ctrls.prog_test.max_value + _ctrls.prog_script.value = _ctrls.prog_script.max_value + + +func _on_gut_start_script(script_obj): + next_script(script_obj.get_full_name(), script_obj.tests.size()) + + +func _on_gut_end_script(): + pass + + +func _on_gut_start_test(test_name): + next_test(test_name) + + +func _on_gut_end_test(): + pass + + +func _on_gut_start_pause(): + pause_before_teardown() + + +func _on_gut_end_pause(): + _ctrls.btn_continue.visible = false + + +func _on_switch_modes_pressed(): + switch_modes.emit() + + +func _on_word_wrap_toggled(toggled): + _ctrls.rtl.autowrap_mode = toggled +# ------------------ +# Public +# ------------------ +func set_num_scripts(val): + _ctrls.prog_script.value = 0 + _ctrls.prog_script.max_value = val + + +func next_script(path, num_tests): + _ctrls.prog_script.value += 1 + _ctrls.prog_test.value = 0 + _ctrls.prog_test.max_value = num_tests + + _ctrls.path_dir.text = path.get_base_dir() + _ctrls.path_file.text = path.get_file() + + +func next_test(__test_name): + _ctrls.prog_test.value += 1 + + +func pause_before_teardown(): + _ctrls.btn_continue.visible = true + + +func set_gut(g): + if(_gut == g): + return + _gut = g + g.start_run.connect(_on_gut_start_run) + g.end_run.connect(_on_gut_end_run) + + g.start_script.connect(_on_gut_start_script) + g.end_script.connect(_on_gut_end_script) + + g.start_test.connect(_on_gut_start_test) + g.end_test.connect(_on_gut_end_test) + + g.start_pause_before_teardown.connect(_on_gut_start_pause) + g.end_pause_before_teardown.connect(_on_gut_end_pause) + +func get_gut(): + return _gut + +func get_textbox(): + return _ctrls.rtl + +func set_elapsed_time(t): + _ctrls.time_label.text = str("%6.1f" % t, 's') + + +func set_bg_color(c): + _ctrls.rtl_bg.color = c + + +func set_title(text): + _ctrls.title.text = text + + +func to_top_left(): + self.position = Vector2(5, 5) + + +func to_bottom_right(): + var win_size = get_display_size() + self.position = win_size - Vector2(self.size) - Vector2(5, 5) + + +func align_right(): + var win_size = get_display_size() + self.position.x = win_size.x - self.size.x -5 + self.position.y = 5 + self.size.y = win_size.y - 10 diff --git a/addons/gut/gui/gut_gui.gd.uid b/addons/gut/gui/gut_gui.gd.uid new file mode 100644 index 0000000..329177a --- /dev/null +++ b/addons/gut/gui/gut_gui.gd.uid @@ -0,0 +1 @@ +uid://blvhsbnsvfyow diff --git a/addons/gut/gui/gut_logo.gd b/addons/gut/gui/gut_logo.gd new file mode 100644 index 0000000..1d88b0d --- /dev/null +++ b/addons/gut/gui/gut_logo.gd @@ -0,0 +1,225 @@ +@tool +extends Node2D + +class GutEyeball: + extends Node2D + + var _should_draw_laser = false + var _laser_end_pos = Vector2.ZERO + var _laser_timer : Timer = null + var _color_tween : Tween + var _size_tween : Tween + + var sprite : Sprite2D = null + var default_position = Vector2(0, 0) + var move_radius = 25 + var move_center = Vector2(0, 0) + var default_color = Color(0.31, 0.31, 0.31) + var _color = default_color : + set(val): + _color = val + queue_redraw() + var color = _color : + set(val): + _start_color_tween(_color, val) + get(): return _color + var default_size = 70 + var _size = default_size : + set(val): + _size = val + queue_redraw() + var size = _size : + set(val): + _start_size_tween(_size, val) + get(): return _size + + + func _init(node): + sprite = node + default_position = sprite.position + move_center = sprite.position + # hijack the original sprite, because I want to draw it here but keep + # the original in the scene for layout. + position = sprite.position + sprite.get_parent().add_child(self) + sprite.visible = false + + + func _ready(): + _laser_timer = Timer.new() + _laser_timer.wait_time = .1 + _laser_timer.one_shot = true + add_child(_laser_timer) + _laser_timer.timeout.connect(func(): _should_draw_laser = false) + + + func _process(_delta): + if(_should_draw_laser): + queue_redraw() + + + func _start_color_tween(old_color, new_color): + if(_color_tween != null and _color_tween.is_running()): + _color_tween.kill() + _color_tween = create_tween() + _color_tween.tween_property(self, '_color', new_color, .3).from(old_color) + _color_tween.play() + + + func _start_size_tween(old_size, new_size): + if(_size_tween != null and _size_tween.is_running()): + _size_tween.kill() + _size_tween = create_tween() + _size_tween.tween_property(self, '_size', new_size, .3).from(old_size) + _size_tween.play() + + + var _laser_size = 20.0 + func _draw() -> void: + draw_circle(Vector2.ZERO, size, color, true, -1, true) + if(_should_draw_laser): + var end_pos = (_laser_end_pos - global_position) * 2 + var laser_size = _laser_size * (float(size)/float(default_size)) + draw_line(Vector2.ZERO, end_pos, color, laser_size) + draw_line(Vector2.ZERO, end_pos, Color(1, 1, 1, .5), laser_size * .8) + + + # There's a bug in here where the eye shakes like crazy. It's a feature + # now. Don't fix it. + func look_at_local_position(local_pos): + var dir = position.direction_to(local_pos) + var dist = position.distance_to(local_pos) + position = move_center + (dir * min(dist, move_radius)) + position.x = clamp(position.x, move_center.x - move_radius, move_center.x + move_radius) + position.y = clamp(position.y, move_center.y - move_radius, move_center.y + move_radius) + + + func reset(): + color = default_color + size = default_size + + + func eye_laser(global_pos): + _should_draw_laser = true + _laser_end_pos = global_pos + _laser_timer.start() + + + func _stop_laser(): + _should_draw_laser = false + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') +# Active means it's actively doing stuff. When this is not active the eyes +# won't follow, but you can still make the sizes change by calling methods on +# this. +@export var active = false : + set(val): + active = val + if(!active and is_inside_tree()): + left_eye.position = left_eye.default_position + right_eye.position = right_eye.default_position +# When disabled, this will reset to default and you can't make it do anything. +@export var disabled = false : + set(val): + disabled = val + if(disabled and is_inside_tree()): + left_eye.position = left_eye.default_position + right_eye.position = right_eye.default_position + left_eye.reset() + right_eye.reset() + modulate = Color.GRAY + $BaseLogo.texture = _no_shine + else: + $BaseLogo.texture = _normal + modulate = Color.WHITE + +@onready var _reset_timer = $ResetTimer +@onready var _face_button = $FaceButton +@onready var left_eye : GutEyeball = GutEyeball.new($BaseLogo/LeftEye) +@onready var right_eye : GutEyeball = GutEyeball.new($BaseLogo/RightEye) + +var _no_shine = load("res://addons/gut/images/GutIconV2_no_shine.png") +var _normal = load("res://addons/gut/images/GutIconV2_base.png") +var _is_in_edited_scene = false + +signal pressed + +func _debug_ready(): + position = Vector2(500, 500) + active = true + + +func _ready(): + _is_in_edited_scene = GutEditorGlobals.is_being_edited_in_editor(self) + + if(get_parent() == get_tree().root): + _debug_ready() + + disabled = disabled + active = active + left_eye.move_center.x -= 20 + right_eye.move_center.x += 10 + _face_button.modulate.a = 0.0 + + +func _process(_delta): + if(active and !disabled and !_is_in_edited_scene): + left_eye.look_at_local_position(get_local_mouse_position()) + right_eye.look_at_local_position(get_local_mouse_position()) + + +# ---------------- +# Events +# ---------------- +func _on_reset_timer_timeout() -> void: + left_eye.reset() + right_eye.reset() + + +func _on_face_button_pressed() -> void: + pressed.emit() + + +# ---------------- +# Public +# ---------------- +func set_eye_scale(left, right=left): + if(disabled or _is_in_edited_scene): + return + left_eye.size = left_eye.default_size * left + right_eye.size = right_eye.default_size * right + _reset_timer.start() + + +func reset_eye_size(): + if(disabled or _is_in_edited_scene): + return + left_eye.size = left_eye.default_size + right_eye.size = right_eye.default_size + + +func set_eye_color(left, right=left): + if(disabled or _is_in_edited_scene): + return + left_eye.color = left + right_eye.color = right + _reset_timer.start() + + +func reset_eye_color(): + if(disabled or _is_in_edited_scene): + return + left_eye.color = left_eye.default_color + right_eye.color = right_eye.default_color + + +# I removed the eye lasers because they aren't ready yet. I've already spent +# too much time on this logo. It's great, I love it...but it's been long +# enough. This gives me, or someone else, something to do later. +#func eye_lasers(global_pos): + #left_eye.eye_laser(global_pos) + #right_eye.eye_laser(global_pos) diff --git a/addons/gut/gui/gut_logo.gd.uid b/addons/gut/gui/gut_logo.gd.uid new file mode 100644 index 0000000..359575d --- /dev/null +++ b/addons/gut/gui/gut_logo.gd.uid @@ -0,0 +1 @@ +uid://b8lvgepb64m8t diff --git a/addons/gut/gui/gut_user_preferences.gd b/addons/gut/gui/gut_user_preferences.gd new file mode 100644 index 0000000..1e22c02 --- /dev/null +++ b/addons/gut/gui/gut_user_preferences.gd @@ -0,0 +1,79 @@ +class GutEditorPref: + var gut_pref_prefix = 'gut/' + var pname = '__not_set__' + var default = null + var value = '__not_set__' + var _settings = null + + func _init(n, d, s): + pname = n + default = d + _settings = s + load_it() + + func _prefstr(): + var to_return = str(gut_pref_prefix, pname) + return to_return + + func save_it(): + _settings.set_setting(_prefstr(), value) + + func load_it(): + if(_settings.has_setting(_prefstr())): + value = _settings.get_setting(_prefstr()) + else: + value = default + + func erase(): + _settings.erase(_prefstr()) + + +const EMPTY = '-- NOT_SET --' + +# -- Editor ONLY Settings -- +var output_font_name = null +var output_font_size = null +var hide_result_tree = null +var hide_output_text = null +var hide_settings = null +var use_colors = null # ? might be output panel +var run_externally = null +var run_externally_options_dialog_size = null +var shortcuts_dialog_size = null +var gut_window_size = null +var gut_window_on_top = null + + +func _init(editor_settings): + output_font_name = GutEditorPref.new('output_font_name', 'CourierPrime', editor_settings) + output_font_size = GutEditorPref.new('output_font_size', 30, editor_settings) + hide_result_tree = GutEditorPref.new('hide_result_tree', false, editor_settings) + hide_output_text = GutEditorPref.new('hide_output_text', false, editor_settings) + hide_settings = GutEditorPref.new('hide_settings', false, editor_settings) + use_colors = GutEditorPref.new('use_colors', true, editor_settings) + run_externally = GutEditorPref.new('run_externally', false, editor_settings) + run_externally_options_dialog_size = GutEditorPref.new('run_externally_options_dialog_size', Vector2i(-1, -1), editor_settings) + shortcuts_dialog_size = GutEditorPref.new('shortcuts_dialog_size', Vector2i(-1, -1), editor_settings) + gut_window_size = GutEditorPref.new('editor_window_size', Vector2i(-1, -1), editor_settings) + gut_window_on_top = GutEditorPref.new('editor_window_on_top', false, editor_settings) + + +func save_it(): + for prop in get_property_list(): + var val = get(prop.name) + if(val is GutEditorPref): + val.save_it() + + +func load_it(): + for prop in get_property_list(): + var val = get(prop.name) + if(val is GutEditorPref): + val.load_it() + + +func erase_all(): + for prop in get_property_list(): + var val = get(prop.name) + if(val is GutEditorPref): + val.erase() diff --git a/addons/gut/gui/gut_user_preferences.gd.uid b/addons/gut/gui/gut_user_preferences.gd.uid new file mode 100644 index 0000000..d4f3d4d --- /dev/null +++ b/addons/gut/gui/gut_user_preferences.gd.uid @@ -0,0 +1 @@ +uid://dsndkn6whyiov diff --git a/addons/gut/gui/option_maker.gd b/addons/gut/gui/option_maker.gd new file mode 100644 index 0000000..a2b1473 --- /dev/null +++ b/addons/gut/gui/option_maker.gd @@ -0,0 +1,124 @@ +var PanelControls = load("res://addons/gut/gui/panel_controls.gd") + +# All titles so we can free them when we want. +var _all_titles = [] + + +var base_container = null +# All the various PanelControls indexed by thier keys. +var controls = {} + + +func _init(cont): + base_container = cont + + +func add_title(text): + var row = PanelControls.BaseGutPanelControl.new(text, text) + base_container.add_child(row) + row.connect('draw', _on_title_cell_draw.bind(row)) + _all_titles.append(row) + return row + + +func add_ctrl(key, ctrl): + controls[key] = ctrl + base_container.add_child(ctrl) + + +func add_number(key, value, disp_text, v_min, v_max, hint=''): + var ctrl = PanelControls.GpcNumber.new(disp_text, value, v_min, v_max, hint) + add_ctrl(key, ctrl) + return ctrl + + +func add_float(key, value, disp_text, step, v_min, v_max, hint=''): + var ctrl = PanelControls.GpcFloat.new(disp_text, value, step, v_min, v_max, hint) + add_ctrl(key, ctrl) + return ctrl + + +func add_select(key, value, values, disp_text, hint=''): + var ctrl = PanelControls.GpcSelect.new(disp_text, value, values, hint) + add_ctrl(key, ctrl) + return ctrl + + +func add_value(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcString.new(disp_text, value, hint) + add_ctrl(key, ctrl) + return ctrl + +func add_multiline_text(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcMultiLineString.new(disp_text, value, hint) + add_ctrl(key, ctrl) + return ctrl + +func add_boolean(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcBoolean.new(disp_text, value, hint) + add_ctrl(key, ctrl) + return ctrl + + +func add_directory(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcDirectory.new(disp_text, value, hint) + add_ctrl(key, ctrl) + ctrl.dialog.title = disp_text + return ctrl + + +func add_file(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcDirectory.new(disp_text, value, hint) + add_ctrl(key, ctrl) + ctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_OPEN_FILE + ctrl.dialog.title = disp_text + return ctrl + + +func add_save_file_anywhere(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcDirectory.new(disp_text, value, hint) + add_ctrl(key, ctrl) + ctrl.dialog.file_mode = ctrl.dialog.FILE_MODE_SAVE_FILE + ctrl.dialog.access = ctrl.dialog.ACCESS_FILESYSTEM + ctrl.dialog.title = disp_text + return ctrl + + +func add_color(key, value, disp_text, hint=''): + var ctrl = PanelControls.GpcColor.new(disp_text, value, hint) + add_ctrl(key, ctrl) + return ctrl + + +var _blurbs = 0 +func add_blurb(text): + var ctrl = RichTextLabel.new() + ctrl.fit_content = true + ctrl.bbcode_enabled = true + ctrl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + ctrl.text = text + add_ctrl(str("blurb_", _blurbs), ctrl) + return ctrl + + +# ------------------ +# Events +# ------------------ +func _on_title_cell_draw(which): + which.draw_rect(Rect2(Vector2(0, 0), which.size), Color(0, 0, 0, .15)) + + +# ------------------ +# Public +# ------------------ + +func clear(): + for key in controls: + controls[key].free() + + controls.clear() + + for entry in _all_titles: + entry.free() + + _all_titles.clear() diff --git a/addons/gut/gui/option_maker.gd.uid b/addons/gut/gui/option_maker.gd.uid new file mode 100644 index 0000000..13a94b4 --- /dev/null +++ b/addons/gut/gui/option_maker.gd.uid @@ -0,0 +1 @@ +uid://bjahqsqo645sf diff --git a/addons/gut/gui/panel_controls.gd b/addons/gut/gui/panel_controls.gd new file mode 100644 index 0000000..227322e --- /dev/null +++ b/addons/gut/gui/panel_controls.gd @@ -0,0 +1,484 @@ +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class BaseGutPanelControl: + extends HBoxContainer + var label = Label.new() + var _lbl_unsaved = Label.new() + var _lbl_invalid = Label.new() + + var value = null: + get: return get_value() + set(val): set_value(val) + + signal changed + + func _init(title, val, hint=""): + size_flags_horizontal = SIZE_EXPAND_FILL + mouse_filter = MOUSE_FILTER_PASS + + label.size_flags_horizontal = label.SIZE_EXPAND_FILL + label.mouse_filter = label.MOUSE_FILTER_STOP + add_child(label) + + _lbl_unsaved.text = '*' + _lbl_unsaved.visible = false + add_child(_lbl_unsaved) + + _lbl_invalid.text = '!' + _lbl_invalid.visible = false + add_child(_lbl_invalid) + + label.text = title + label.tooltip_text = hint + + + func mark_unsaved(is_it=true): + _lbl_unsaved.visible = is_it + + + func mark_invalid(is_it): + _lbl_invalid.visible = is_it + + # -- Virtual -- + # + # value_ctrl (all should declare the value_ctrl) + # + func set_value(value): + pass + + func get_value(): + pass + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcNumber: + extends BaseGutPanelControl + + var value_ctrl = SpinBox.new() + + func _init(title, val, v_min, v_max, hint=""): + super._init(title, val, hint) + + value_ctrl.value = val + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.min_value = v_min + value_ctrl.max_value = v_max + value_ctrl.value_changed.connect(_on_value_changed) + value_ctrl.select_all_on_focus = true + add_child(value_ctrl) + + func _on_value_changed(new_value): + changed.emit() + + func get_value(): + return value_ctrl.value + + func set_value(val): + value_ctrl.value = val + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcFloat: + extends GpcNumber + + func _init(title, val, step, v_min, v_max, hint=""): + super._init(title, val, v_min, v_max, hint) + value_ctrl.step = step + value_ctrl.value = val + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcString: + extends BaseGutPanelControl + + var value_ctrl = LineEdit.new() + + func _init(title, val, hint=""): + super._init(title, val, hint) + + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.text = val + value_ctrl.text_changed.connect(_on_text_changed) + value_ctrl.select_all_on_focus = true + add_child(value_ctrl) + if(title == ''): + label.visible = false + + func _on_text_changed(new_value): + changed.emit() + + func get_value(): + return value_ctrl.text + + func set_value(val): + value_ctrl.text = val + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcMultiLineString: + extends BaseGutPanelControl + + var value_ctrl = TextEdit.new() + + func _init(title, val, hint=""): + super._init(title, val, hint) + var vbox = VBoxContainer.new() + vbox.size_flags_horizontal = SIZE_EXPAND_FILL + add_child(vbox) + label.reparent(vbox) + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.text = val + value_ctrl.text_changed.connect(_on_text_changed) + value_ctrl.scroll_fit_content_height = true + vbox.add_child(value_ctrl) + + func _on_text_changed(new_value): + changed.emit() + + func get_value(): + return value_ctrl.text + + func set_value(val): + value_ctrl.text = val + + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcBoolean: + extends BaseGutPanelControl + + var value_ctrl = CheckBox.new() + + func _init(title, val, hint=""): + super._init(title, val, hint) + + value_ctrl.button_pressed = val + value_ctrl.toggled.connect(_on_button_toggled) + add_child(value_ctrl) + + func _on_button_toggled(new_value): + changed.emit() + + func get_value(): + return value_ctrl.button_pressed + + func set_value(val): + value_ctrl.button_pressed = val + + +# ------------------------------------------------------------------------------ +# value is "selected" and is gettable and settable +# text is the text value of the selected item, it is gettable only +# ------------------------------------------------------------------------------ +class GpcSelect: + extends BaseGutPanelControl + + var value_ctrl = OptionButton.new() + + var text = '' : + get: return value_ctrl.get_item_text(value_ctrl.selected) + set(val): pass + + func _init(title, val, choices, hint=""): + super._init(title, val, hint) + + var select_idx = 0 + for i in range(choices.size()): + value_ctrl.add_item(choices[i]) + if(val == choices[i]): + select_idx = i + value_ctrl.selected = select_idx + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.item_selected.connect(_on_item_selected) + add_child(value_ctrl) + + func _on_item_selected(idx): + changed.emit() + + func get_value(): + return value_ctrl.selected + + func set_value(val): + value_ctrl.selected = val + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcColor: + extends BaseGutPanelControl + + var value_ctrl = ColorPickerButton.new() + + func _init(title, val, hint=""): + super._init(title, val, hint) + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.color = val + add_child(value_ctrl) + + func get_value(): + return value_ctrl.color + + func set_value(val): + value_ctrl.color = val + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcDirectory: + extends BaseGutPanelControl + + var value_ctrl := LineEdit.new() + var dialog := FileDialog.new() + var enabled_button = CheckButton.new() + + var _btn_dir := Button.new() + + func _init(title, val, hint=""): + super._init(title, val, hint) + + label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + + _btn_dir.text = '...' + _btn_dir.pressed.connect(_on_dir_button_pressed) + + value_ctrl.text = val + value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL + value_ctrl.select_all_on_focus = true + value_ctrl.text_changed.connect(_on_value_changed) + + dialog.file_mode = dialog.FILE_MODE_OPEN_DIR + dialog.unresizable = false + dialog.dir_selected.connect(_on_selected) + dialog.file_selected.connect(_on_selected) + + enabled_button.button_pressed = true + enabled_button.visible = false + + add_child(enabled_button) + add_child(value_ctrl) + add_child(_btn_dir) + add_child(dialog) + + func _update_display(): + var is_empty = value_ctrl.text == '' + enabled_button.button_pressed = !is_empty + enabled_button.disabled = is_empty + + + func _ready(): + if(Engine.is_editor_hint()): + dialog.size = Vector2(1000, 700) + else: + dialog.size = Vector2(500, 350) + _update_display() + + func _on_value_changed(new_text): + _update_display() + + func _on_selected(path): + value_ctrl.text = path + _update_display() + + func _on_dir_button_pressed(): + dialog.current_dir = value_ctrl.text + dialog.popup_centered() + + func get_value(): + return value_ctrl.text + + func set_value(val): + value_ctrl.text = val + + +# ------------------------------------------------------------------------------ +# Features: +# Buttons to pick res://, user://, or anywhere on the OS. +# ------------------------------------------------------------------------------ +class GpcFileDialogSuperPlus: + extends FileDialog + + var show_diretory_types = true : + set(val) : + show_diretory_types = val + _update_display() + + var show_res = true : + set(val) : + show_res = val + _update_display() + + var show_user = true : + set(val) : + show_user = val + _update_display() + + var show_os = true : + set(val) : + show_os = val + _update_display() + + var _dir_type_hbox = null + var _btn_res = null + var _btn_user = null + var _btn_os = null + + func _ready(): + _init_controls() + _update_display() + + + func _init_controls(): + _dir_type_hbox = HBoxContainer.new() + + _btn_res = Button.new() + _btn_user = Button.new() + _btn_os = Button.new() + var spacer1 = CenterContainer.new() + spacer1.size_flags_horizontal = spacer1.SIZE_EXPAND_FILL + var spacer2 = spacer1.duplicate() + + _dir_type_hbox.add_child(spacer1) + _dir_type_hbox.add_child(_btn_res) + _dir_type_hbox.add_child(_btn_user) + _dir_type_hbox.add_child(_btn_os) + _dir_type_hbox.add_child(spacer2) + + _btn_res.text = 'res://' + _btn_user.text = 'user://' + _btn_os.text = ' OS ' + + get_vbox().add_child(_dir_type_hbox) + get_vbox().move_child(_dir_type_hbox, 0) + + _btn_res.pressed.connect(func(): access = ACCESS_RESOURCES) + _btn_user.pressed.connect(func(): access = ACCESS_USERDATA) + _btn_os.pressed.connect(func(): access = ACCESS_FILESYSTEM) + + + func _update_display(): + if(is_inside_tree()): + _dir_type_hbox.visible = show_diretory_types + _btn_res.visible = show_res + _btn_user.visible = show_user + _btn_os.visible = show_os + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +class GpcSaveLoad: + extends BaseGutPanelControl + + var btn_load = Button.new() + var btn_save = Button.new() + + var dlg_load := GpcFileDialogSuperPlus.new() + var dlg_save := GpcFileDialogSuperPlus.new() + + signal save_path_chosen(path) + signal load_path_chosen(path) + + func _init(title, val, hint): + super._init(title, val, hint) + + btn_load.text = "Load" + btn_load.custom_minimum_size.x = 100 + btn_load.pressed.connect(_on_load_pressed) + add_child(btn_load) + + btn_save.text = "Save As" + btn_save.custom_minimum_size.x = 100 + btn_save.pressed.connect(_on_save_pressed) + add_child(btn_save) + + dlg_load.file_mode = dlg_load.FILE_MODE_OPEN_FILE + dlg_load.unresizable = false + dlg_load.dir_selected.connect(_on_load_selected) + dlg_load.file_selected.connect(_on_load_selected) + add_child(dlg_load) + + dlg_save.file_mode = dlg_save.FILE_MODE_SAVE_FILE + dlg_save.unresizable = false + dlg_save.dir_selected.connect(_on_save_selected) + dlg_save.file_selected.connect(_on_save_selected) + add_child(dlg_save) + + + func _ready(): + if(Engine.is_editor_hint()): + dlg_load.size = Vector2(1000, 700) + dlg_save.size = Vector2(1000, 700) + else: + dlg_load.size = Vector2(500, 350) + dlg_save.size = Vector2(500, 350) + + func _on_load_selected(path): + load_path_chosen.emit(path) + + func _on_save_selected(path): + save_path_chosen.emit(path) + + func _on_load_pressed(): + dlg_load.popup_centered() + + func _on_save_pressed(): + dlg_save.popup_centered() + +# ------------------------------------------------------------------------------ +# This one was never used in gut_config_gui...but I put some work into it and +# I'm a sucker for that kinda thing. Delete this when you get tired of looking +# at it. +# ------------------------------------------------------------------------------ +# class Vector2Ctrl: +# extends VBoxContainer + +# var value = Vector2(-1, -1) : +# get: +# return get_value() +# set(val): +# set_value(val) +# var disabled = false : +# get: +# return get_disabled() +# set(val): +# set_disabled(val) +# var x_spin = SpinBox.new() +# var y_spin = SpinBox.new() + +# func _init(): +# add_child(_make_one('x: ', x_spin)) +# add_child(_make_one('y: ', y_spin)) + +# func _make_one(txt, spinner): +# var hbox = HBoxContainer.new() +# var lbl = Label.new() +# lbl.text = txt +# hbox.add_child(lbl) +# hbox.add_child(spinner) +# spinner.min_value = -1 +# spinner.max_value = 10000 +# spinner.size_flags_horizontal = spinner.SIZE_EXPAND_FILL +# return hbox + +# func set_value(v): +# if(v != null): +# x_spin.value = v[0] +# y_spin.value = v[1] + +# # Returns array instead of vector2 b/c that is what is stored in +# # in the dictionary and what is expected everywhere else. +# func get_value(): +# return [x_spin.value, y_spin.value] + +# func set_disabled(should): +# get_parent().visible = !should +# x_spin.visible = !should +# y_spin.visible = !should + +# func get_disabled(): +# pass diff --git a/addons/gut/gui/panel_controls.gd.uid b/addons/gut/gui/panel_controls.gd.uid new file mode 100644 index 0000000..3b6d8f4 --- /dev/null +++ b/addons/gut/gui/panel_controls.gd.uid @@ -0,0 +1 @@ +uid://db54jy04d8w7p diff --git a/addons/gut/gui/play.png b/addons/gut/gui/play.png new file mode 100644 index 0000000000000000000000000000000000000000..06fbff3bf95daacb8079f76ebd4230627ac7fb09 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe4i*Lm2K9!kZ43+yVV*9IAr-fJPc0N|2@qgC z==+LKM$B=WhYD-USB)b#>~HVV4&V05V^SDj-X@n#E@xfNyPTJ-JON^4>(7g{k(>Ei tIa4d6NiD-kUC{Ht)JK;)pI5hu|F&7QD*hFNHv void: + GutLoader = load("res://addons/gut/gut_loader.gd") + + +@warning_ignore("unsafe_method_access") +func _ready() -> void: + _post_ready.call_deferred() + + +func _post_ready(): + var runner : Node = load("res://addons/gut/gui/GutRunner.tscn").instantiate() + get_tree().root.add_child(runner) + runner.run_from_editor() diff --git a/addons/gut/gui/run_from_editor.gd.uid b/addons/gut/gui/run_from_editor.gd.uid new file mode 100644 index 0000000..b782883 --- /dev/null +++ b/addons/gut/gui/run_from_editor.gd.uid @@ -0,0 +1 @@ +uid://bwf2iuidqfkpl diff --git a/addons/gut/gui/run_from_editor.tscn b/addons/gut/gui/run_from_editor.tscn new file mode 100644 index 0000000..a979090 --- /dev/null +++ b/addons/gut/gui/run_from_editor.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bgj3fm5d8yvjw"] + +[ext_resource type="Script" uid="uid://bwf2iuidqfkpl" path="res://addons/gut/gui/run_from_editor.gd" id="1_53pap"] + +[node name="RunFromEditor" type="Node2D"] +script = ExtResource("1_53pap") diff --git a/addons/gut/gui/update_required.gd b/addons/gut/gui/update_required.gd new file mode 100644 index 0000000..55edfc3 --- /dev/null +++ b/addons/gut/gui/update_required.gd @@ -0,0 +1,30 @@ +@tool +extends AcceptDialog + +var should_continue = false +var _check_for_update_ctrl = null + +signal closed + +func _ready(): + add_cancel_button("Cancel Loading GUT") + + +func _on_confirmed() -> void: + should_continue = true + closed.emit.call_deferred() + + +func _on_canceled() -> void: + should_continue = false + closed.emit.call_deferred() + + +func _on_close_requested() -> void: + should_continue = false + closed.emit.call_deferred() + + +func set_check_for_update_control(ctrl): + _check_for_update_ctrl = ctrl + add_child(ctrl) diff --git a/addons/gut/gui/update_required.gd.uid b/addons/gut/gui/update_required.gd.uid new file mode 100644 index 0000000..972c2ac --- /dev/null +++ b/addons/gut/gui/update_required.gd.uid @@ -0,0 +1 @@ +uid://bpj4k0w6ft550 diff --git a/addons/gut/gui/update_required.tscn b/addons/gut/gui/update_required.tscn new file mode 100644 index 0000000..bfd21a9 --- /dev/null +++ b/addons/gut/gui/update_required.tscn @@ -0,0 +1,17 @@ +[gd_scene format=3 uid="uid://sduiv3grt5h4"] + +[ext_resource type="Script" uid="uid://bpj4k0w6ft550" path="res://addons/gut/gui/update_required.gd" id="1_rmy01"] + +[node name="UpdateRequired" type="AcceptDialog" unique_id=125541884] +oversampling_override = 1.0 +title = "Invalid GUT Version" +position = Vector2i(0, 36) +size = Vector2i(600, 300) +visible = true +exclusive = false +ok_button_text = "Continue" +script = ExtResource("1_rmy01") + +[connection signal="canceled" from="." to="." method="_on_canceled"] +[connection signal="close_requested" from="." to="." method="_on_close_requested"] +[connection signal="confirmed" from="." to="." method="_on_confirmed"] diff --git a/addons/gut/gut.gd b/addons/gut/gut.gd new file mode 100644 index 0000000..cd8f198 --- /dev/null +++ b/addons/gut/gut.gd @@ -0,0 +1,1278 @@ +extends 'res://addons/gut/gut_to_move.gd' +class_name GutMain +## The GUT brains. +## +## Most of this class is for internal use only. Features that can be used +## have descriptions and can be accessed through the [member GutTest.gut] variable +## in your test scripts (extends [GutTest]). +## The wiki page for this class contains only the usable features. +## [br][br] +## GUT Wiki: [url=https://gut.readthedocs.io]https://gut.readthedocs.io[/url] +## [br] +## @ignore-uncommented + + +# --------------------------- +# Constants +# --------------------------- +const LOG_LEVEL_FAIL_ONLY = 0 +const LOG_LEVEL_TEST_AND_FAILURES = 1 +const LOG_LEVEL_ALL_ASSERTS = 2 +const WAITING_MESSAGE = '/# waiting #/' +const PAUSE_MESSAGE = '/# Pausing. Press continue button...#/' +const COMPLETED = 'completed' + +# --------------------------- +# Signals +# --------------------------- +signal start_pause_before_teardown +signal end_pause_before_teardown + +signal start_run +signal end_run +## Emitted before every test script instance is created. +## Emitted before [method GutTest.before_all] hook on test is run. +## test_script_obj is an instance of addons/gut/collected_script.gd. +signal start_script(test_script_obj) +## Emitted after every test script is run. Emitted after [method GutTest.after_all] hook on test is run. +signal end_script +## Emitted before every test method is run. Emitted after [method GutTest.before_each] hook on test is run. +## test_name is the string name of the current test about to be started. +signal start_test(test_name) +## Emitted after every test method is run. Emitted after [method GutTest.after_each] hook on test is run. +signal end_test + + +# --------------------------- +# Settings +# +# These are properties that are usually set before a run is started through +# gutconfig. +# --------------------------- + +var _inner_class_name = '' +# When set, GUT will only run Inner-Test-Classes that contain this string. +var inner_class_name = _inner_class_name : + get: return _inner_class_name + set(val): _inner_class_name = val + +var _ignore_pause_before_teardown = false +# For batch processing purposes, you may want to ignore any calls to +# pause_before_teardown that you forgot to remove_at. +var ignore_pause_before_teardown = _ignore_pause_before_teardown : + get: return _ignore_pause_before_teardown + set(val): _ignore_pause_before_teardown = val + +var _log_level = 1 +## The log detail level. Valid values are 0 - 2. Larger values do not matter. +var log_level = _log_level: + get: return _log_level + set(val): _set_log_level(val) + +## The amount of time that must elapse before an "Awaiting" message is printed. +var wait_log_delay = 0.5 + +# TODO 4.0 +# This appears to not be used anymore. Going to wait for more tests to be +# ported before removing. +var _disable_strict_datatype_checks = false +var disable_strict_datatype_checks = false : + get: return _disable_strict_datatype_checks + set(val): _disable_strict_datatype_checks = val + +var _export_path = '' +# Path to file that GUT will create which holds a list of all test scripts so +# that GUT can run tests when a project is exported. +var export_path = '' : + get: return _export_path + set(val): _export_path = val + +var _include_subdirectories = false +# Setting this to true will make GUT search all subdirectories of any directory +# you have configured GUT to search for tests in. +var include_subdirectories = _include_subdirectories : + get: return _include_subdirectories + set(val): _include_subdirectories = val + + +var _double_strategy = GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY +# TODO rework what this is and then document it here. +var double_strategy = _double_strategy : + get: return _double_strategy + set(val): + if(GutUtils.DOUBLE_STRATEGY.values().has(val)): + _double_strategy = val + _doubler.set_strategy(double_strategy) + else: + _lgr.error(str("gut.gd: invalid double_strategy ", val)) + +var _pre_run_script = '' +# Path to the script that will be run before all tests are run. This script +# must extend GutHookScript +var pre_run_script = _pre_run_script : + get: return _pre_run_script + set(val): _pre_run_script = val + +var _post_run_script = '' +# Path to the script that will run after all tests have run. The script +# must extend GutHookScript +var post_run_script = _post_run_script : + get: return _post_run_script + set(val): _post_run_script = val + +var _color_output = false +# Flag to color output at the command line and in the GUT GUI. +var color_output = false : + get: return _color_output + set(val): + _color_output = val + _lgr.disable_formatting(!_color_output) + +var _junit_xml_file = '' +# The full path to where GUT should write a JUnit compliant XML file to which +# contains the results of all tests run. +var junit_xml_file = '' : + get: return _junit_xml_file + set(val): _junit_xml_file = val + +var _junit_xml_timestamp = false +# When true and junit_xml_file is set, the file name will include a +# timestamp so that previous files are not overwritten. +var junit_xml_timestamp = false : + get: return _junit_xml_timestamp + set(val): _junit_xml_timestamp = val + +# The minimum amout of time GUT will wait before pausing for 1 frame to allow +# the screen to paint. GUT checkes after each test to see if enough time has +# passed. +var paint_after = .1: + get: return paint_after + set(val): paint_after = val + +var _unit_test_name = '' +# When set GUT will only run tests that contain this string. +var unit_test_name = _unit_test_name : + get: return _unit_test_name + set(val): _unit_test_name = val + +var _parameter_handler = null +# This is populated by test.gd each time a paramterized test is encountered +# for the first time. +# FOR INTERNAL USE ONLY +var parameter_handler = _parameter_handler : + get: return _parameter_handler + set(val): + _parameter_handler = val + _parameter_handler.set_logger(_lgr) + +var _lgr = GutUtils.get_logger() +# Local reference for the common logger. +var logger = _lgr : + get: return _lgr + set(val): + _lgr = val + _lgr.set_gut(self) + +var error_tracker = GutUtils.get_error_tracker() + +var _add_children_to = self +# Sets the object that GUT will add test objects to as it creates them. The +# default is self, but can be set to other objects so that GUT is not obscured +# by the objects added during tests. +var add_children_to = self : + get: return _add_children_to + set(val): _add_children_to = val + + +# ------------ +# Read only +# ------------ +var _test_collector = GutUtils.TestCollector.new() +func get_test_collector(): + return _test_collector + +# var version = null : +func get_version(): + return GutUtils.version_numbers.gut_version + +var _orphan_counter = GutUtils.OrphanCounter.new() +func get_orphan_counter(): + return _orphan_counter + +# var _autofree = GutUtils.AutoFree.new() +func get_autofree(): + return _orphan_counter.autofree + +var _stubber = GutUtils.Stubber.new() +func get_stubber(): + return _stubber + +var _doubler = GutUtils.Doubler.new() +func get_doubler(): + return _doubler + +var _spy = GutUtils.Spy.new() +func get_spy(): + return _spy + +var _is_running = false +func is_running(): + return _is_running + + +# --------------------------- +# Private +# --------------------------- +var _should_print_versions = true # used to cut down on output in tests. +var _should_print_summary = true + +var _file_prefix = 'test_' +var _inner_class_prefix = 'Test' + +var _select_script = '' +var _last_paint_time = 0.0 +var _strutils = GutUtils.Strutils.new() + +# The instance that is created from _pre_run_script. Accessible from +# get_pre_run_script_instance. These are created at the start of the run +# and then referenced at the appropriate time. This allows us to validate the +# scripts prior to running. +var _pre_run_script_instance = null +var _post_run_script_instance = null + +var _script_name = null + +# The instanced scripts. This is populated as the scripts are run. +var _test_script_objects = [] + +var _waiting = false + +# msecs ticks when run was started +var _start_time = 0.0 + +# Collected Test instance for the current test being run. +var _current_test = null +var _pause_before_teardown = false + + +# Used to cancel importing scripts if an error has occurred in the setup. This +# prevents tests from being run if they were exported and ensures that the +# error displayed is seen since importing generates a lot of text. +# +# TODO this appears to only be checked and never set anywhere. Verify that this +# was not broken somewhere and remove if no longer used. +var _cancel_import = false + +# this is how long Gut will wait when there are items that must be queued free +# when a test completes (due to calls to add_child_autoqfree) +var _auto_queue_free_delay = .1 + +var _time_to_wait_for_final_queue_free = .5 +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _init(override_logger=null): + if(override_logger != null): + logger = override_logger + else: + logger = logger # force setter logic + + _doubler.set_stubber(_stubber) + _doubler.set_spy(_spy) + _doubler.set_gut(self) + + update_loggers() + +# Public for tests that set the logger. This makes it much easier to propigate +# test loggers. +func update_loggers(): + _doubler.set_logger(_lgr) + _spy.set_logger(_lgr) + _stubber.set_logger(_lgr) + _test_collector.set_logger(_lgr) + + +# ------------------------------------------------------------------------------ +# Initialize controls +# ------------------------------------------------------------------------------ +func _ready(): + if(_should_print_versions): + _lgr.log('--- GUT ---') + _lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.')) + + if(_select_script != null): + select_script(_select_script) + + _print_versions() + +# ------------------------------------------------------------------------------ +# Runs right before free is called. Can't override `free`. +# ------------------------------------------------------------------------------ +func _notification(what): + if(what == NOTIFICATION_PREDELETE): + for ts in _test_script_objects: + if(is_instance_valid(ts)): + ts.free() + + _test_script_objects = [] + + +func _print_versions(send_all = true): + if(!_should_print_versions): + return + + var info = GutUtils.version_numbers.get_version_text() + + if(send_all): + p(info) + else: + _lgr.get_printer('gui').send(info + "\n") + + + + +# --------------------------- +# +# Accessor code +# +# --------------------------- + + +# ------------------------------------------------------------------------------ +# Set the log level. Use one of the various LOG_LEVEL_* constants. +# ------------------------------------------------------------------------------ +func _set_log_level(level): + _log_level = max(level, 0) + + # Level 0 settings + _lgr.set_less_test_names(level == 0) + # Explicitly always enabled + _lgr.set_type_enabled(_lgr.types.normal, true) + _lgr.set_type_enabled(_lgr.types.error, true) + _lgr.set_type_enabled(_lgr.types.pending, true) + + # Level 1 types + _lgr.set_type_enabled(_lgr.types.warn, level > 0) + _lgr.set_type_enabled(_lgr.types.deprecated, level > 0) + + # Level 2 types + _lgr.set_type_enabled(_lgr.types.passed, level > 1) + _lgr.set_type_enabled(_lgr.types.info, level > 1) + _lgr.set_type_enabled(_lgr.types.debug, level > 1) + +# --------------------------- +# +# Events +# +# --------------------------- +func end_teardown_pause(): + _pause_before_teardown = false + _waiting = false + end_pause_before_teardown.emit() + +# --------------------------- +# +# Private +# +# --------------------------- +func _log_test_children_warning(test_script): + if(!_lgr.is_type_enabled(_lgr.types.orphan)): + return + + var kids = test_script.get_children() + if(kids.size() > 1): + var msg = '' + if(_log_level == 2): + msg = "Test script still has children when all tests finisehd.\n" + for i in range(kids.size()): + msg += str(" ", _strutils.type2str(kids[i]), "\n") + msg += "You can use autofree, autoqfree, add_child_autofree, or add_child_autoqfree to automatically free objects." + else: + msg = str("Test script has ", kids.size(), " unfreed children. Increase log level for more details.") + + _lgr.warn(msg) + + +func _log_end_run(): + var summary = GutUtils.Summary.new(self) + if(_should_print_summary): + _orphan_counter.record_orphans("end_run") + if(_lgr.is_type_enabled("orphan") and _orphan_counter.get_count() > 0): + _lgr.log("\n\n\n") + _lgr.orphan("==============================================") + _lgr.orphan(str('= ', _orphan_counter.get_count(), ' Orphans')) + _lgr.orphan("==============================================") + _orphan_counter.log_all() + _lgr.log("\n") + else: + _lgr.log("\n\n\n") + + summary.log_end_run() + + +func _validate_hook_script(path): + var result = { + valid = true, + instance = null + } + + # empty path is valid but will have a null instance + if(path == ''): + return result + + if(FileAccess.file_exists(path)): + var inst = load(path).new() + if(inst and inst is GutHookScript): + result.instance = inst + result.valid = true + else: + result.valid = false + _lgr.error('The hook script [' + path + '] does not extend GutHookScript') + else: + result.valid = false + _lgr.error('The hook script [' + path + '] does not exist.') + + return result + + +# ------------------------------------------------------------------------------ +# Runs a hook script. Script must exist, and must extend +# GutHookScript or addons/gut/hook_script.gd +# ------------------------------------------------------------------------------ +func _run_hook_script(inst): + if(inst != null): + inst.gut = self + await inst.run() + return inst + + +# ------------------------------------------------------------------------------ +# Initialize variables for each run of a single test script. +# ------------------------------------------------------------------------------ +func _init_run(): + var valid = true + _test_collector.set_test_class_prefix(_inner_class_prefix) + _test_script_objects = [] + _current_test = null + _is_running = true + + var pre_hook_result = _validate_hook_script(_pre_run_script) + _pre_run_script_instance = pre_hook_result.instance + var post_hook_result = _validate_hook_script(_post_run_script) + _post_run_script_instance = post_hook_result.instance + + valid = pre_hook_result.valid and post_hook_result.valid + + return valid + + +# ------------------------------------------------------------------------------ +# Print out run information and close out the run. +# ------------------------------------------------------------------------------ +func _end_run(): + await _run_hook_script(get_post_run_script_instance()) + + _orphan_counter.record_orphans("end_run") + _orphan_counter.orphanage.clean() + _log_end_run() + _is_running = false + + _export_results() + end_run.emit() + + +# ------------------------------------------------------------------------------ +# Add additional export types here. +# ------------------------------------------------------------------------------ +func _export_results(): + if(_junit_xml_file != ''): + _export_junit_xml() + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _export_junit_xml(): + var exporter = GutUtils.JunitXmlExport.new() + var output_file = _junit_xml_file + + if(_junit_xml_timestamp): + var ext = "." + output_file.get_extension() + output_file = output_file.replace(ext, str("_", Time.get_unix_time_from_system(), ext)) + + var f_result = exporter.write_file(self, output_file) + if(f_result == OK): + p(str("Results saved to ", output_file)) + + +# ------------------------------------------------------------------------------ +# Print out the heading for a new script +# ------------------------------------------------------------------------------ +func _print_script_heading(coll_script): + if(_does_class_name_match(_inner_class_name, coll_script.inner_class_name)): + _lgr.log(str("\n\n", coll_script.get_full_name()), _lgr.fmts.underline) + + +# ------------------------------------------------------------------------------ +# Yes if the class name is null or the script's class name includes class_name +# ------------------------------------------------------------------------------ +func _does_class_name_match(the_class_name, script_class_name): + return (the_class_name == null or the_class_name == '') or \ + (script_class_name != null and str(script_class_name).findn(the_class_name) != -1) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _create_script_instance(collected_script): + var test_script = collected_script.get_new() + + test_script.gut = self + test_script.set_logger(_lgr) + _add_children_to.add_child(test_script) + _test_script_objects.append(test_script) + test_script.wait_log_delay = wait_log_delay + + if(!test_script._was_ready_called): + test_script._do_ready_stuff() + _lgr.warn(str("!!! YOU HAVE UPSET YOUR GUT !!!\n", + "You have overridden _ready in [", collected_script.get_filename_and_inner(), "] ", + "but it does not call super._ready(). New additions (or maybe old ", + "by the time you see this) require that super._ready() is called.", + "\n\n", + "GUT is working around this infraction, but may not be able to in ", + "the future. GUT also reserves the right to decide it does not want ", + "to work around it in the future. ", + "You should probably use before_all instead of _ready. I can think ", + "of a few reasons why you would want to use _ready but I won't list ", + "them here because I think they are bad ideas. I know they are bad ", + "ideas because I did them. Hence the warning. This message is ", + "intentially long so that it bothers you and you change your ways.\n\n", + "Thank you for using GUT.")) + return test_script + + +# ------------------------------------------------------------------------------ +# returns self so it can be integrated into the yield call. +# ------------------------------------------------------------------------------ +func _wait_for_continue_button(): + p(PAUSE_MESSAGE, 0) + _waiting = true + return self + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _get_indexes_matching_script_name(script_name): + var indexes = [] # empty runs all + for i in range(_test_collector.scripts.size()): + if(_test_collector.scripts[i].get_filename().find(script_name) != -1): + indexes.append(i) + return indexes + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _get_indexes_matching_path(path): + var indexes = [] + for i in range(_test_collector.scripts.size()): + if(_test_collector.scripts[i].path == path): + indexes.append(i) + return indexes + + +# ------------------------------------------------------------------------------ +# Execute all calls of a parameterized test. +# ------------------------------------------------------------------------------ +func _run_parameterized_test(test_script, test_name): + await _run_test(test_script, test_name, 0) + + if(_current_test.assert_count == 0 and !_current_test.pending): + _lgr.risky('Test did not assert') + + if(_parameter_handler == null): + _lgr.error(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.')) + _fail(str('Parameterized test ', _current_test.name, ' did not call use_parameters for the default value of the parameter.')) + else: + var index = 1 + while(!_parameter_handler.is_done()): + var cur_assert_count = _current_test.assert_count + await _run_test(test_script, test_name, index) + if(_current_test.assert_count == cur_assert_count and !_current_test.pending): + _lgr.risky('Test did not assert') + index += 1 + + _parameter_handler = null + + +# ------------------------------------------------------------------------------ +# Runs a single test given a test.gd instance and the name of the test to run. +# ------------------------------------------------------------------------------ +func _run_test(script_inst, test_name, param_index = -1): + _lgr.log_test_name() + _lgr.set_indent_level(1) + + await script_inst.before_each() + + start_test.emit(test_name) + var test_id = str(script_inst.collected_script.get_filename_and_inner(), ':', test_name) + if(param_index != -1): + test_id += str('[', param_index, ']') + error_tracker.start_test(test_id) + + # Reset the time and frame tracking stats of the test + script_inst.reset_start_times() + await script_inst.call(test_name) + + if(error_tracker.should_test_fail_from_errors(test_id)): + script_inst._fail(str("Unexpected Errors:\n", error_tracker.get_fail_text_for_errors(test_id))) + + error_tracker.end_test() + # if the test called pause_before_teardown then await until + # the continue button is pressed. + if(_pause_before_teardown and !_ignore_pause_before_teardown): + start_pause_before_teardown.emit() + await _wait_for_continue_button().end_pause_before_teardown + + script_inst.clear_signal_watcher() + + await script_inst.after_each() + + # Free up everything in the _autofree. Yield for a bit if we + # have anything with a queue_free so that they have time to + # free and are not found by the orphan counter. + var aqf_count = _orphan_counter.autofree.get_queue_free_count() + _orphan_counter.autofree.free_all() + if(aqf_count > 0): + await get_tree().create_timer(_auto_queue_free_delay).timeout + + _orphan_counter.end_test( + script_inst.collected_script.get_filename_and_inner(), test_name, + _log_level > 0) + + _doubler.get_ignored_methods().clear() + + +func get_current_test_orphans(): + var to_return = [] + var ct = get_current_test_object() + if(ct.collected_script != null): + var sname = ct.collected_script.get_ref().get_filename_and_inner() + if(ct.name == &'after_all' or ct.name == &'before_all'): + _orphan_counter.record_orphans(sname) + to_return = _orphan_counter.get_orphan_ids(sname) + to_return.append_array(_orphan_counter.get_orphan_ids(ct.collected_script.get_ref().get_full_name())) + else: + to_return = _orphan_counter.record_orphans(sname, ct.name) + + return to_return + + +# ------------------------------------------------------------------------------ +# Calls before_all on the passed in test script and takes care of settings so all +# logger output appears indented and with a proper heading +# +# Calls both pre-all-tests methods until prerun_setup is removed +# ------------------------------------------------------------------------------ +func _call_before_all(test_script, collected_script): + var before_all_test_obj = GutUtils.CollectedTest.new() + before_all_test_obj.has_printed_name = false + before_all_test_obj.name = 'before_all' + + collected_script.setup_teardown_tests.append(before_all_test_obj) + _current_test = before_all_test_obj + _current_test.collected_script = weakref(collected_script) + + _lgr.inc_indent() + await test_script.before_all() + # before all does not need to assert anything so only mark it as run if + # some assert was done. + before_all_test_obj.was_run = before_all_test_obj.did_something() + + _lgr.dec_indent() + + _current_test = null + + +# ------------------------------------------------------------------------------ +# Calls after_all on the passed in test script and takes care of settings so all +# logger output appears indented and with a proper heading +# +# Calls both post-all-tests methods until postrun_teardown is removed. +# ------------------------------------------------------------------------------ +func _call_after_all(test_script, collected_script): + var after_all_test_obj = GutUtils.CollectedTest.new() + after_all_test_obj.has_printed_name = false + after_all_test_obj.name = 'after_all' + + collected_script.setup_teardown_tests.append(after_all_test_obj) + _current_test = after_all_test_obj + _current_test.collected_script = weakref(collected_script) + + _lgr.inc_indent() + await test_script.after_all() + # after all does not need to assert anything so only mark it as run if + # some assert was done. + after_all_test_obj.was_run = after_all_test_obj.did_something() + _lgr.dec_indent() + + _current_test = null + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _should_skip_script(test_script, collected_script): + var skip_message = 'not skipped' + var skip_value = test_script.get('skip_script') + var should_skip = false + + if(skip_value == null): + skip_value = await test_script.should_skip_script() + else: + _lgr.deprecated('Using the skip_script var has been deprecated. Implement the new should_skip_script() method in your test instead.') + + if(skip_value != null): + if(typeof(skip_value) == TYPE_BOOL): + should_skip = skip_value + if(skip_value): + skip_message = 'script marked to skip' + elif(typeof(skip_value) == TYPE_STRING): + should_skip = true + skip_message = skip_value + + if(should_skip): + var msg = str('- [Script skipped]: ', skip_message) + _lgr.inc_indent() + _lgr.log(msg, _lgr.fmts.yellow) + _lgr.dec_indent() + collected_script.skip_reason = skip_message + collected_script.was_skipped = true + + return should_skip + + +# ------------------------------------------------------------------------------ +# Run all tests in a script. This is the core logic for running tests. +# ------------------------------------------------------------------------------ +func _test_the_scripts(indexes=[]): + + _print_versions(false) + var is_valid = _init_run() + if(!is_valid): + _lgr.error('Something went wrong and the run was aborted.') + return + + await _run_hook_script(get_pre_run_script_instance()) + if(_pre_run_script_instance!= null and _pre_run_script_instance.should_abort()): + _lgr.error('pre-run abort') + end_run.emit() + return + + start_run.emit() + _start_time = Time.get_ticks_msec() + _last_paint_time = _start_time + + var indexes_to_run = [] + if(indexes.size()==0): + for i in range(_test_collector.scripts.size()): + indexes_to_run.append(i) + else: + indexes_to_run = indexes + + + # loop through scripts + for test_indexes in range(indexes_to_run.size()): + var coll_script = _test_collector.scripts[indexes_to_run[test_indexes]] + + if(coll_script.tests.size() > 0): + _lgr.set_indent_level(0) + _print_script_heading(coll_script) + + if(!coll_script.is_loaded): + break + + start_script.emit(coll_script) + + var test_script = _create_script_instance(coll_script) + _doubler.set_strategy(_double_strategy) + + # ---- + # SHORTCIRCUIT + # skip_script logic + if(await _should_skip_script(test_script, coll_script)): + _orphan_counter.record_orphans(coll_script.get_full_name()) + continue + # ---- + + # !!! + # Hack so there isn't another indent to this monster of a method. if + # inner class is set and we do not have a match then empty the tests + # for the current test. + # !!! + if(!_does_class_name_match(_inner_class_name, coll_script.inner_class_name)): + coll_script.tests = [] + else: + coll_script.was_run = true + await _call_before_all(test_script, coll_script) + + _orphan_counter.record_orphans(coll_script.get_full_name()) + # Each test in the script + for i in range(coll_script.tests.size()): + _stubber.clear() + _spy.clear() + _current_test = coll_script.tests[i] + + if((_unit_test_name != '' and _current_test.name.find(_unit_test_name) > -1) or + (_unit_test_name == '')): + + var ticks_before := Time.get_ticks_usec() + + if(_current_test.arg_count > 1): + _lgr.error(str('Parameterized test ', _current_test.name, + ' has too many parameters: ', _current_test.arg_count, '.')) + elif(_current_test.arg_count == 1): + _current_test.was_run = true + await _run_parameterized_test(test_script, _current_test.name) + else: + _current_test.was_run = true + await _run_test(test_script, _current_test.name) + + if(!_current_test.did_something()): + _lgr.risky(str(_current_test.name, ' did not assert')) + + _current_test.has_printed_name = false + + _current_test.time_taken = (Time.get_ticks_usec() - ticks_before) / 1000000.0 + + end_test.emit() + + # After each test, check to see if we shoudl wait a frame to + # paint based on how much time has elapsed since we last 'painted' + if(paint_after > 0.0): + var now = Time.get_ticks_msec() + var time_since = (now - _last_paint_time) / 1000.0 + if(time_since > paint_after): + _last_paint_time = now + await get_tree().process_frame + + _current_test = null + _lgr.dec_indent() + + if(_does_class_name_match(_inner_class_name, coll_script.inner_class_name)): + await _call_after_all(test_script, coll_script) + + _orphan_counter.end_script( + coll_script.get_filename_and_inner(), + _log_level > 0) + + _log_test_children_warning(test_script) + # This might end up being very resource intensive if the scripts + # don't clean up after themselves. Might have to consolidate output + # into some other structure and kill the script objects with + # test_script.free() instead of remove_at child. + _add_children_to.remove_child(test_script) + + _lgr.set_indent_level(0) + if(test_script.get_assert_count() > 0): + var script_sum = str(coll_script.get_passing_test_count(), '/', coll_script.get_ran_test_count(), ' passed.') + _lgr.log(script_sum, _lgr.fmts.bold) + + test_script.queue_free() + end_script.emit() + # END TEST SCRIPT LOOP + + _lgr.set_indent_level(0) + # Give anything that is queued to be freed time to be freed before we count + # the orphans. Without this, the last test's awaiter won't be freed + # yet, which messes with the orphans total. There could also be objects + # the user has queued to be freed as well. + # Bump number from .1 to .5 when inner classes that were not run were still + # appearing as orphans. Maybe this could loop through the orpahns looking + # for entries that were not freed but are queued to be freed and wait unitl + # they are all gone. ".5" is a lot easier. + await get_tree().create_timer(_time_to_wait_for_final_queue_free).timeout + _end_run() + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _pass(text=''): + if(_current_test): + _current_test.add_pass(text) + + +# ------------------------------------------------------------------------------ +# Returns an empty string or "(call #x) " if the current test being run has +# parameters. The +# ------------------------------------------------------------------------------ +func get_call_count_text(): + var to_return = '' + if(_parameter_handler != null): + # This uses get_call_count -1 because test.gd's use_parameters method + # should have been called before we get to any calls for this method + # just due to how use_parameters works. There isn't a way to know + # whether we are before or after that call. + to_return = str('params[', _parameter_handler.get_call_count() -1, '] ') + return to_return + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _fail(text=''): + if(_current_test != null): + var line_number = _extract_line_number(_current_test) + var line_text = ' at line ' + str(line_number) + p(line_text, LOG_LEVEL_FAIL_ONLY) + # format for summary + line_text = "\n " + line_text + var call_count_text = get_call_count_text() + _current_test.line_number = line_number + _current_test.add_fail(call_count_text + text + line_text) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _pending(text=''): + if(_current_test): + _current_test.add_pending(text) + + +# ------------------------------------------------------------------------------ +# Extracts the line number from curren stacktrace by matching the test case name +# ------------------------------------------------------------------------------ +func _extract_line_number(current_test): + var line_number = -1 + # if stack trace available than extraxt the test case line number + var stackTrace = get_stack() + if(stackTrace!=null): + for index in stackTrace.size(): + var line = stackTrace[index] + var function = line.get("function") + if function == current_test.name: + line_number = line.get("line") + return line_number + + +# ------------------------------------------------------------------------------ +# Gets all the files in a directory and all subdirectories if include_subdirectories +# is true. The files returned are all sorted by name. +# ------------------------------------------------------------------------------ +func _get_files(path, prefix, suffix): + var files = [] + var directories = [] + # ignore addons/gut per issue 294 + if(path == 'res://addons/gut'): + return []; + + var d = DirAccess.open(path) + d.include_hidden = false + d.include_navigational = false + + # Traversing a directory is kinda odd. You have to start the process of + # listing the contents of a directory with list_dir_begin then use get_next + # until it returns an empty string. Then I guess you should end it. + d.list_dir_begin() + var fs_item = d.get_next() + var full_path = '' + while(fs_item != ''): + full_path = path.path_join(fs_item) + + # MUST use FileAccess since d.file_exists returns false for exported + # projects + if(FileAccess.file_exists(full_path)): + if(fs_item.begins_with(prefix) and fs_item.ends_with(suffix)): + files.append(full_path) + # MUST use DirAccess, d.dir_exists is false for exported projects. + elif(include_subdirectories and DirAccess.dir_exists_absolute(full_path)): + directories.append(full_path) + + fs_item = d.get_next() + d.list_dir_end() + + for dir in range(directories.size()): + var dir_files = _get_files(directories[dir], prefix, suffix) + for i in range(dir_files.size()): + files.append(dir_files[i]) + + files.sort() + return files + + +# --------------------------- +# +# public +# +# --------------------------- + +func get_elapsed_time() -> float: + var to_return = 0.0 + if(_start_time != 0.0): + to_return = Time.get_ticks_msec() - _start_time + to_return = to_return / 1000.0 + + return to_return + +# ------------------------------------------------------------------------------ +# Conditionally prints the text to the console/results variable based on the +# current log level and what level is passed in. Whenever currently in a test, +# the text will be indented under the test. It can be further indented if +# desired. +# +# The first time output is generated when in a test, the test name will be +# printed. +# ------------------------------------------------------------------------------ +func p(text, level=0): + var str_text = str(text) + + if(level <= GutUtils.nvl(_log_level, 0)): + _lgr.log(str_text) + +# --------------------------- +# +# RUN TESTS/ADD SCRIPTS +# +# --------------------------- + +# ------------------------------------------------------------------------------ +# Runs all the scripts that were added using add_script +# ------------------------------------------------------------------------------ +func test_scripts(_run_rest=false): + if(_script_name != null and _script_name != ''): + var indexes = _get_indexes_matching_script_name(_script_name) + if(indexes == []): + _lgr.error(str( + "Could not find script matching '", _script_name, "'.\n", + "Check your directory settings and Script Prefix/Suffix settings.")) + end_run.emit() + else: + _test_the_scripts(indexes) + else: + _test_the_scripts([]) + +# alias +func run_tests(run_rest=false): + test_scripts(run_rest) + + +# ------------------------------------------------------------------------------ +# Runs a single script passed in. +# ------------------------------------------------------------------------------ +# func run_test_script(script): +# _test_collector.set_test_class_prefix(_inner_class_prefix) +# _test_collector.clear() +# _test_collector.add_script(script) +# _test_the_scripts() + + +# ------------------------------------------------------------------------------ +# Adds a script to be run when test_scripts called. +# ------------------------------------------------------------------------------ +func add_script(script): + # if(!Engine.is_editor_hint()): + _test_collector.set_test_class_prefix(_inner_class_prefix) + _test_collector.add_script(script) + + +# ------------------------------------------------------------------------------ +# Add all scripts in the specified directory that start with the prefix and end +# with the suffix. Does not look in sub directories. Can be called multiple +# times. +# ------------------------------------------------------------------------------ +func add_directory(path, prefix=_file_prefix, suffix=".gd"): + # check for '' b/c the calls to addin the exported directories 1-6 will pass + # '' if the field has not been populated. This will cause res:// to be + # processed which will include all files if include_subdirectories is true. + if(path == '' or path == null): + return + + var dir = DirAccess.open(path) + if(dir == null): + _lgr.error(str('The path [', path, '] does not exist.')) + else: + var files = _get_files(path, prefix, suffix) + for i in range(files.size()): + if(_script_name == null or _script_name == '' or \ + (_script_name != null and files[i].findn(_script_name) != -1)): + add_script(files[i]) + + +# ------------------------------------------------------------------------------ +# This will try to find a script in the list of scripts to test that contains +# the specified script name. It does not have to be a full match. It will +# select the first matching occurrence so that this script will run when run_tests +# is called. Works the same as the select_this_one option of add_script. +# +# returns whether it found a match or not +# ------------------------------------------------------------------------------ +func select_script(script_name): + _script_name = script_name + _select_script = script_name + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func export_tests(path=_export_path): + if(path == null): + _lgr.error('You must pass a path or set the export_path before calling export_tests') + else: + var result = _test_collector.export_tests(path) + if(result): + _lgr.info(_test_collector.to_s()) + _lgr.info("Exported to " + path) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func import_tests(path=_export_path): + if(!FileAccess.file_exists(path)): + _lgr.error(str('Cannot import tests: the path [', path, '] does not exist.')) + else: + _test_collector.clear() + var result = _test_collector.import_tests(path) + if(result): + _lgr.info("\n" + _test_collector.to_s()) + _lgr.info("Imported from " + path) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func import_tests_if_none_found(): + if(!_cancel_import and _test_collector.scripts.size() == 0): + import_tests() + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func export_if_tests_found(): + if(_test_collector.scripts.size() > 0): + export_tests() + +# --------------------------- +# +# MISC +# +# --------------------------- + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func maximize(): + _lgr.deprecated('gut.maximize') + + +# ------------------------------------------------------------------------------ +# Clears the text of the text box. This resets all counters. +# ------------------------------------------------------------------------------ +func clear_text(): + _lgr.deprecated('gut.clear_text') + + +# ------------------------------------------------------------------------------ +# Get the number of tests that were ran +# ------------------------------------------------------------------------------ +func get_test_count(): + return _test_collector.get_ran_test_count() + +# ------------------------------------------------------------------------------ +## Get the number of assertions that were made +func get_assert_count(): + return _test_collector.get_assert_count() + +# ------------------------------------------------------------------------------ +## Get the number of assertions that passed +func get_pass_count(): + return _test_collector.get_pass_count() + +# ------------------------------------------------------------------------------ +## Get the number of assertions that failed +func get_fail_count(): + return _test_collector.get_fail_count() + +# ------------------------------------------------------------------------------ +## Get the number of tests flagged as pending +func get_pending_count(): + return _test_collector.get_pending_count() + + +# ------------------------------------------------------------------------------ +# Call this method to make the test pause before teardown so that you can inspect +# anything that you have rendered to the screen. +# ------------------------------------------------------------------------------ +func pause_before_teardown(): + _pause_before_teardown = true; + + +# ------------------------------------------------------------------------------ +# Returns the script object instance that is currently being run. +# ------------------------------------------------------------------------------ +func get_current_script_object(): + var to_return = null + if(_test_script_objects.size() > 0): + to_return = _test_script_objects[-1] + return to_return + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_current_test_object(): + return _current_test + + +## Returns a summary.gd object that contains all the information about +## the run results. +func get_summary(): + return GutUtils.Summary.new(self) + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_pre_run_script_instance(): + return _pre_run_script_instance + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_post_run_script_instance(): + return _post_run_script_instance + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func show_orphans(should): + _lgr.set_type_enabled(_lgr.types.orphan, should) + + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_logger(): + return _lgr + + +# ------------------------------------------------------------------------------ +## Returns the number of test scripts. Inner Test classes each count as a +## script. +func get_test_script_count(): + return _test_script_objects.size() + + + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gut.gd.uid b/addons/gut/gut.gd.uid new file mode 100644 index 0000000..8b84514 --- /dev/null +++ b/addons/gut/gut.gd.uid @@ -0,0 +1 @@ +uid://duvvxvj18c04d diff --git a/addons/gut/gut_cmdln.gd b/addons/gut/gut_cmdln.gd new file mode 100644 index 0000000..51c2937 --- /dev/null +++ b/addons/gut/gut_cmdln.gd @@ -0,0 +1,71 @@ +# ------------------------------------------------------------------------------ +# Description +# ----------- +# Entry point for the command line interface. The actual logic for GUT's CLI +# is in addons/gut/cli/gut_cli.gd. +# +# This script should conform to, or ignore, the strictest warning settings. +# ------------------------------------------------------------------------------ +extends SceneTree + +var VersionConversion = load("res://addons/gut/version_conversion.gd") + +@warning_ignore("unsafe_method_access") +@warning_ignore("inferred_declaration") +func _init() -> void: + if(VersionConversion.error_if_not_all_classes_imported()): + quit(0) + return + + var max_iter := 20 + var iter := 0 + + # Make a reference to gut_loader so its _static_init is run. + var Laoder : Object = load("res://addons/gut/gut_loader.gd") + + # Not seen this wait more than 1. + while(Engine.get_main_loop() == null and iter < max_iter): + await create_timer(.01).timeout + iter += 1 + + if(Engine.get_main_loop() == null): + push_error('Main loop did not start in time.') + quit(0) + return + + var cli : Node = load('res://addons/gut/cli/gut_cli.gd').new() + get_root().add_child(cli) + + cli.main() + + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gut_cmdln.gd.uid b/addons/gut/gut_cmdln.gd.uid new file mode 100644 index 0000000..4766ae7 --- /dev/null +++ b/addons/gut/gut_cmdln.gd.uid @@ -0,0 +1 @@ +uid://bxw30gwbwnh55 diff --git a/addons/gut/gut_config.gd b/addons/gut/gut_config.gd new file mode 100644 index 0000000..0d4f949 --- /dev/null +++ b/addons/gut/gut_config.gd @@ -0,0 +1,236 @@ +# ############################################################################## +# +# This holds all the configuratoin values for GUT. It can load and save values +# to a json file. It is also responsible for applying these settings to GUT. +# +# ############################################################################## +const FAIL_ERROR_TYPE_ENGINE = &'engine' +const FAIL_ERROR_TYPE_PUSH_ERROR = &'push_error' +const FAIL_ERROR_TYPE_GUT = &'gut' + + + +var valid_fonts = GutUtils.gut_fonts.get_font_names() +var _deprecated_values = { + "errors_do_not_cause_failure": "Use failure_error_types instead." +} + +var default_options = { + background_color = Color(.15, .15, .15, 1).to_html(), + config_file = 'res://.gutconfig.json', + # used by editor to handle enabled/disabled dirs. All dirs configured go + # here and only the enabled dirs go into dirs + configured_dirs = [], + dirs = [], + disable_colors = false, + # double strategy can be the name of the enum value, the enum value or + # lowercase name with spaces: 0/SCRIPT_ONLY/script only + # The GUI gut config expects the value to be the enum value and not a string + # when saved. + double_strategy = 'SCRIPT_ONLY', + font_color = Color(.8, .8, .8, 1).to_html(), + font_name = GutUtils.gut_fonts.DEFAULT_CUSTOM_FONT_NAME, + font_size = 16, + hide_orphans = false, + ignore_pause = false, + include_subdirs = false, + inner_class = '', + junit_xml_file = '', + junit_xml_timestamp = false, + log_level = 1, + opacity = 100, + paint_after = .1, + post_run_script = '', + pre_run_script = '', + prefix = 'test_', + selected = '', + should_exit_on_success = false, + should_exit = false, + should_maximize = false, + compact_mode = false, + show_help = false, + suffix = '.gd', + tests = [], + unit_test_name = '', + + no_error_tracking = false, + failure_error_types = ["engine", "gut", "push_error"], + wait_log_delay = .5, + + gut_on_top = true, +} + + +var options = default_options.duplicate() +var logger = GutUtils.get_logger() + +func _null_copy(h): + var new_hash = {} + for key in h: + new_hash[key] = null + return new_hash + + +func _load_options_from_config_file(file_path, into): + if(!FileAccess.file_exists(file_path)): + # Default files are ok to be missing. Maybe this is too deep a place + # to implement this, but here it is. + if(file_path != 'res://.gutconfig.json' and file_path != GutUtils.EditorGlobals.editor_run_gut_config_path): + logger.error(str('Config File "', file_path, '" does not exist.')) + return -1 + else: + return 1 + + var f = FileAccess.open(file_path, FileAccess.READ) + if(f == null): + var result = FileAccess.get_open_error() + logger.error(str("Could not load data ", file_path, ' ', result)) + return result + + var json = f.get_as_text() + f = null # close file + + var test_json_conv = JSON.new() + test_json_conv.parse(json) + var results = test_json_conv.get_data() + # SHORTCIRCUIT + if(results == null): + logger.error(str("Could not parse file: ", file_path)) + return -1 + + # Get all the options out of the config file using the option name. The + # options hash is now the default source of truth for the name of an option. + _load_dict_into(results, into) + + return 1 + + +func _load_dict_into(source, dest): + for key in dest: + if(source.has(key)): + if(source[key] != null): + if(typeof(source[key]) == TYPE_DICTIONARY): + _load_dict_into(source[key], dest[key]) + else: + dest[key] = source[key] + + +# Apply all the options specified to tester. This is where the rubber meets +# the road. +func _apply_options(opts, gut): + for entry in _deprecated_values.keys(): + if(opts.has(entry)): + # Use gut.logger instead of our own for testing purposes. + logger.deprecated(str('Config value "', entry, '" is deprecated. ', _deprecated_values[entry])) + + gut.include_subdirectories = opts.include_subdirs + + if(opts.inner_class != ''): + gut.inner_class_name = opts.inner_class + gut.log_level = opts.log_level + gut.ignore_pause_before_teardown = opts.ignore_pause + + gut.select_script(opts.selected) + + for i in range(opts.dirs.size()): + gut.add_directory(opts.dirs[i], opts.prefix, opts.suffix) + + for i in range(opts.tests.size()): + gut.add_script(opts.tests[i]) + + # Sometimes it is the index, sometimes it's a string. This sets it regardless + gut.double_strategy = GutUtils.get_enum_value( + opts.double_strategy, GutUtils.DOUBLE_STRATEGY, + GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY) + + gut.unit_test_name = opts.unit_test_name + gut.pre_run_script = opts.pre_run_script + gut.post_run_script = opts.post_run_script + gut.color_output = !opts.disable_colors + gut.show_orphans(!opts.hide_orphans) + gut.junit_xml_file = opts.junit_xml_file + gut.junit_xml_timestamp = opts.junit_xml_timestamp + gut.paint_after = str(opts.paint_after).to_float() + gut.wait_log_delay = opts.wait_log_delay + + # These error_tracker options default to true. Don't trust this comment. + if(!opts.failure_error_types.has(FAIL_ERROR_TYPE_ENGINE)): + gut.error_tracker.treat_engine_errors_as = GutUtils.TREAT_AS.NOTHING + + if(!opts.failure_error_types.has(FAIL_ERROR_TYPE_PUSH_ERROR)): + gut.error_tracker.treat_push_error_as = GutUtils.TREAT_AS.NOTHING + + if(!opts.failure_error_types.has(FAIL_ERROR_TYPE_GUT)): + gut.error_tracker.treat_gut_errors_as = GutUtils.TREAT_AS.NOTHING + + gut.error_tracker.register_loggers = !opts.no_error_tracking + + return gut + + +# -------------------------- +# Public +# -------------------------- +func write_options(path): + var content = JSON.stringify(options, ' ') + + var f = FileAccess.open(path, FileAccess.WRITE) + var result = FileAccess.get_open_error() + if(f != null): + f.store_string(content) + f = null # closes file + else: + logger.error(str("Could not open file ", path, ' ', result)) + return result + + +# consistent name +func save_file(path): + write_options(path) + + +func load_options(path): + return _load_options_from_config_file(path, options) + + +# consistent name +func load_file(path): + return load_options(path) + + +func load_options_no_defaults(path): + options = _null_copy(default_options) + return _load_options_from_config_file(path, options) + + +func apply_options(gut): + _apply_options(options, gut) + + + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gut_config.gd.uid b/addons/gut/gut_config.gd.uid new file mode 100644 index 0000000..a164476 --- /dev/null +++ b/addons/gut/gut_config.gd.uid @@ -0,0 +1 @@ +uid://bobi58361x1ya diff --git a/addons/gut/gut_constants.gd b/addons/gut/gut_constants.gd new file mode 100644 index 0000000..e1b964d --- /dev/null +++ b/addons/gut/gut_constants.gd @@ -0,0 +1,130 @@ +class_name GutConstants + +const TYPE_STRINGS = { + TYPE_NIL : 'TYPE_NIL', + TYPE_BOOL : 'TYPE_BOOL', + TYPE_INT : 'TYPE_INT', + TYPE_FLOAT : 'TYPE_FLOAT', + TYPE_STRING : 'TYPE_STRING', + TYPE_VECTOR2 : 'TYPE_VECTOR2', + TYPE_VECTOR2I : 'TYPE_VECTOR2I', + TYPE_RECT2 : 'TYPE_RECT2', + TYPE_RECT2I : 'TYPE_RECT2I', + TYPE_VECTOR3 : 'TYPE_VECTOR3', + TYPE_VECTOR3I : 'TYPE_VECTOR3I', + TYPE_TRANSFORM2D : 'TYPE_TRANSFORM2D', + TYPE_VECTOR4 : 'TYPE_VECTOR4', + TYPE_VECTOR4I : 'TYPE_VECTOR4I', + TYPE_PLANE : 'TYPE_PLANE', + TYPE_QUATERNION : 'TYPE_QUATERNION', + TYPE_AABB : 'TYPE_AABB', + TYPE_BASIS : 'TYPE_BASIS', + TYPE_TRANSFORM3D : 'TYPE_TRANSFORM3D', + TYPE_PROJECTION : 'TYPE_PROJECTION', + TYPE_COLOR : 'TYPE_COLOR', + TYPE_STRING_NAME : 'TYPE_STRING_NAME', + TYPE_NODE_PATH : 'TYPE_NODE_PATH', + TYPE_RID : 'TYPE_RID', + TYPE_OBJECT : 'TYPE_OBJECT', + TYPE_CALLABLE : 'TYPE_CALLABLE', + TYPE_SIGNAL : 'TYPE_SIGNAL', + TYPE_DICTIONARY : 'TYPE_DICTIONARY', + TYPE_ARRAY : 'TYPE_ARRAY', + TYPE_PACKED_BYTE_ARRAY : 'TYPE_PACKED_BYTE_ARRAY', + TYPE_PACKED_INT32_ARRAY : 'TYPE_PACKED_INT32_ARRAY', + TYPE_PACKED_INT64_ARRAY : 'TYPE_PACKED_INT64_ARRAY', + TYPE_PACKED_FLOAT32_ARRAY : 'TYPE_PACKED_FLOAT32_ARRAY', + TYPE_PACKED_FLOAT64_ARRAY : 'TYPE_PACKED_FLOAT64_ARRAY', + TYPE_PACKED_STRING_ARRAY : 'TYPE_PACKED_STRING_ARRAY', + TYPE_PACKED_VECTOR2_ARRAY : 'TYPE_PACKED_VECTOR2_ARRAY', + TYPE_PACKED_VECTOR3_ARRAY : 'TYPE_PACKED_VECTOR3_ARRAY', + TYPE_PACKED_COLOR_ARRAY : 'TYPE_PACKED_COLOR_ARRAY', + TYPE_PACKED_VECTOR4_ARRAY : 'TYPE_PACKED_VECTOR4_ARRAY', + TYPE_MAX : 'TYPE_MAX', +} + +# Values should not be referenced outside of this script. +# Use get_default_return_value. The functions look weird, but they are here +# so that each call to a doubled method returns a unique instance of things for +# anything that would be passed by reference. +static var _default_returns = { + TYPE_NIL : null, + TYPE_BOOL : false, + TYPE_INT : 0, + TYPE_FLOAT : 0.0, + TYPE_STRING : '', + TYPE_VECTOR2 : Vector2.ZERO, + TYPE_VECTOR2I : Vector2i.ZERO, + TYPE_RECT2 : func(): return Rect2(0, 0, 0, 0), + TYPE_RECT2I : func(): return Rect2i(0, 0, 0, 0), + TYPE_VECTOR3 : Vector3.ZERO, + TYPE_VECTOR3I : Vector3i.ZERO, + TYPE_TRANSFORM2D : Transform2D.IDENTITY, + TYPE_VECTOR4 : Vector4.ZERO, + TYPE_VECTOR4I : Vector4i.ZERO, + TYPE_PLANE : Plane.PLANE_XY, + TYPE_QUATERNION : Quaternion.IDENTITY, + TYPE_AABB : func(): return AABB(), + TYPE_BASIS : Basis.IDENTITY, + TYPE_TRANSFORM3D : Transform3D.IDENTITY, + TYPE_PROJECTION : Projection.IDENTITY, + TYPE_COLOR : Color.WHITE, + TYPE_STRING_NAME : &'', + TYPE_NODE_PATH : func(): return NodePath(), + TYPE_RID : func(): return RID(), + TYPE_OBJECT : null, + TYPE_CALLABLE : func(): return Callable(), + TYPE_SIGNAL : null, + TYPE_DICTIONARY : func(): return {}, + TYPE_ARRAY : func(): return [], + TYPE_PACKED_BYTE_ARRAY : func(): return PackedByteArray(), + TYPE_PACKED_INT32_ARRAY : func(): return PackedInt32Array(), + TYPE_PACKED_INT64_ARRAY : func(): return PackedInt64Array(), + TYPE_PACKED_FLOAT32_ARRAY : func(): return PackedFloat32Array(), + TYPE_PACKED_FLOAT64_ARRAY : func(): return PackedFloat64Array(), + TYPE_PACKED_STRING_ARRAY : func(): return PackedStringArray(), + TYPE_PACKED_VECTOR2_ARRAY : func(): return PackedVector2Array(), + TYPE_PACKED_VECTOR3_ARRAY : func(): return PackedVector3Array(), + TYPE_PACKED_COLOR_ARRAY : func(): return PackedColorArray(), + TYPE_PACKED_VECTOR4_ARRAY : func(): return PackedVector4Array(), + # TYPE_MAX : 'TYPE_MAX', +} + +# Seeded with any type constant where the constant name can't be converted to +# a string using pascal case and/or the values need manual conversion. The +# rest are added in _static_init. +static var TYPE_KEYWORDS = { + TYPE_NIL : 'null', + TYPE_BOOL : 'bool', + TYPE_INT : 'int', + TYPE_FLOAT : 'float', +} + + +static var NOT_SET := &"___NOT__SET___" + + +static func _static_init() -> void: + for key in TYPE_STRINGS: + if(!TYPE_KEYWORDS.has(key)): + var n : String = TYPE_STRINGS[key].to_lower() + n = n.lstrip("type_") + TYPE_KEYWORDS[key] = n.to_pascal_case() + + +static func is_not_set(val): + return typeof(val) == TYPE_STRING_NAME and val == NOT_SET + + +static func get_default_return_value(type=null): + if(type == null or typeof(type) != TYPE_INT): + return null + var to_return = null + if(is_not_set(type)): + to_return = null + elif(_default_returns.has(type)): + to_return = _default_returns[type] + if(to_return is Callable): + to_return = to_return.call() + + return to_return \ No newline at end of file diff --git a/addons/gut/gut_constants.gd.uid b/addons/gut/gut_constants.gd.uid new file mode 100644 index 0000000..ef3bd6b --- /dev/null +++ b/addons/gut/gut_constants.gd.uid @@ -0,0 +1 @@ +uid://brilp5mefn3ye diff --git a/addons/gut/gut_fonts.gd b/addons/gut/gut_fonts.gd new file mode 100644 index 0000000..875aef6 --- /dev/null +++ b/addons/gut/gut_fonts.gd @@ -0,0 +1,94 @@ +# ------------------------------------------------------------------------------ +# There was an error that someone found in Godot 4.4.1, but ended up being a +# different error in Godot 4.5. The fix was to hold a reference to the font +# so that TextEdit control did not lose the font when switching. This is +# the solution I came up with. Just hold a reference to all fonts we use, +# but only when we use them. Basically a lazy loader with some semantics for +# font names and location. +# +# https://github.com/bitwes/Gut/issues/749 +# +# An instance of this could be used to allow users to specify their own fonts. +# It's not perect for that yet, but it is feasible. +# ------------------------------------------------------------------------------ +const DEFAULT_CUSTOM_FONT_NAME = 'CourierPrime' +const THEME_FONT_TO_FONT_TYPES_MAP = { + 'font':FONT_TYPES.REGULAR, + 'normal_font': FONT_TYPES.REGULAR, + 'bold_font': FONT_TYPES.BOLD, + 'italics_font':FONT_TYPES.ITALIC, + 'bold_italics_font':FONT_TYPES.BOLD_ITALIC +} + + +# Values for FONT_TYPES are based on Google font file suffix (not extension). +# A font file will be a key from fonts + - + FONT_TYPE value + .ttf. +const FONT_TYPES = { + REGULAR = 'Regular', + BOLD = 'Bold', + ITALIC = 'Italic', + BOLD_ITALIC = 'BoldItalic' +} + + +var fonts = { + 'AnonymousPro':{}, + 'CourierPrime':{}, + 'LobsterTwo':{}, + 'Default':{} +} + + +var custom_font_path = 'res://addons/gut/fonts/' + + +func _init(): + _populate_default_fonts() + + +func _populate_default_fonts(): + var ctrl = TextEdit.new() + var f = ctrl.get_theme_font('font') + for key in FONT_TYPES: + fonts['Default'][FONT_TYPES[key]] = f + ctrl.free() + + +func _load_font(font_name, font_type, font_path): + var dynamic_font = FontFile.new() + dynamic_font.load_dynamic_font(font_path) + fonts[font_name][font_type] = dynamic_font + + +func get_font(font_name, font_type='Regular'): + if(!fonts.has(font_name)): + push_error(str("Invalid font name '", font_name, "'")) + return fonts['Default'][FONT_TYPES.REGULAR] + + if(!FONT_TYPES.values().has(font_type)): + push_error(str("Invalid font type '", font_type, "'")) + return fonts['Default'][FONT_TYPES.REGULAR] + + if(!fonts[font_name].has(font_type)): + var filename = custom_font_path.path_join(str(font_name, '-', font_type, '.ttf')) + if(FileAccess.file_exists(filename)): + _load_font(font_name, font_type, filename) + else: + push_error(str("Missing custom font ", filename)) + return fonts['Default'][FONT_TYPES.REGULAR] + + return fonts.get(font_name, {}).get(font_type, null) + + +func get_font_names(): + return fonts.keys() + + +# Maps the various theme font names (font, normal_font, italics_font etc) to +# a FONT_TYPE. +func get_font_for_theme_font_name(theme_font_name, custom_font_name): + if(!THEME_FONT_TO_FONT_TYPES_MAP.has(theme_font_name)): + push_error(str("Unknown theme font name ", theme_font_name)) + return get_font(custom_font_name) + return get_font(custom_font_name, THEME_FONT_TO_FONT_TYPES_MAP[theme_font_name]) + diff --git a/addons/gut/gut_fonts.gd.uid b/addons/gut/gut_fonts.gd.uid new file mode 100644 index 0000000..12d85fb --- /dev/null +++ b/addons/gut/gut_fonts.gd.uid @@ -0,0 +1 @@ +uid://dvajwe2cllerq diff --git a/addons/gut/gut_loader.gd b/addons/gut/gut_loader.gd new file mode 100644 index 0000000..28d3f38 --- /dev/null +++ b/addons/gut/gut_loader.gd @@ -0,0 +1,75 @@ +# ------------------------------------------------------------------------------ +# This script has been greatly simplified by Godot 4.6's ability to include or +# exclude individual directories. This now just excludes the gut directory +# right off the bat and that's about it. +# +# This script should be loaded as soon as possible when running tests. This +# will disable warnings and then load all scripts that are registered with the +# LazyLoader. This also adds res://addons/gut to directory_rules for exclusion. +## +# This was created after a first attempt to suppress all GUT warnings did not +# work for the strictest warning settings. This has turned the LazyLoader into +# just a Loader...so maybe all that should be reworked or renamed. A problem +# for a time when we are absolutely sure that all warnings are being correctly +# suppressed I suppose. +# +# You can use the cli script addons/gut/cli/change_project_warnings.gd to +# quickly alter project warning levels for testing purposes. +# gdscript addons/gut/cli/change_project_warnings.gd --headless ++ -h +# +# You can set project warning settings from the command line with: +# godot -s addons/gut/cli/change_project_warnings.gd ++ -h +# +# This script should conform to, or ignore, the strictest warning settings. +# ------------------------------------------------------------------------------ +const WARNING_PATH : String = 'debug/gdscript/warnings/' + + +@warning_ignore("unsafe_method_access") +@warning_ignore("unsafe_property_access") +@warning_ignore("untyped_declaration") +static func _static_init() -> void: + var WarningsManager = load('res://addons/gut/warnings_manager.gd') + if(!WarningsManager.disabled): + WarningsManager.exclude_gut() + WarningsManager.exclude_dynamic_files() + + # Force a reference to utils.gd by path. Using the class_name would cause + # utils.gd to load when this script loads, before we could turn off the + # warnings. + var _utils : Object = load('res://addons/gut/utils.gd') + + # Since load_all exists on the LazyLoader, it should be done now so nothing + # sneaks in later...This essentially defeats the "lazy" part of the + # LazyLoader, but not the "loader" part of LazyLoader. + _utils.LazyLoader.load_all() + + +# ############################################################################## +# (G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gut_loader.gd.uid b/addons/gut/gut_loader.gd.uid new file mode 100644 index 0000000..de5b25d --- /dev/null +++ b/addons/gut/gut_loader.gd.uid @@ -0,0 +1 @@ +uid://2jdhrg7xws31 diff --git a/addons/gut/gut_loader_the_scene.tscn b/addons/gut/gut_loader_the_scene.tscn new file mode 100644 index 0000000..8fbd010 --- /dev/null +++ b/addons/gut/gut_loader_the_scene.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://jt6wsefn0x54"] + +[sub_resource type="Resource" id="Resource_cayac"] +metadata/__load_path__ = "res://addons/gut/gut_loader_the_scene.gd" + +[node name="Node" type="Node2D"] +script = SubResource("Resource_cayac") diff --git a/addons/gut/gut_menu.gd b/addons/gut/gut_menu.gd new file mode 100644 index 0000000..9e5d055 --- /dev/null +++ b/addons/gut/gut_menu.gd @@ -0,0 +1,104 @@ +var sub_menu : PopupMenu = null + +var _menus = { + # name : { + # index, + # id, + # callback + # } +} + +signal about +signal rerun +signal run_all +signal run_at_cursor +signal run_inner_class +signal run_script +signal run_test +signal show_gut +signal toggle_windowed + + +func _init(): + sub_menu = PopupMenu.new() + sub_menu.index_pressed.connect(_on_sub_menu_index_pressed) + make_menu() + + +func _invalid_index(): + print("bad menu index") + + +func _on_sub_menu_index_pressed(index): + var to_call : Callable = _invalid_index + for key in _menus: + if(_menus[key].index == index): + to_call = _menus[key].callback + + to_call.call() + + +func add_menu(display_text, sig_to_emit, tooltip=''): + var index = sub_menu.item_count + _menus[sig_to_emit.get_name()] = { + index = index, + id = index, + callback = sig_to_emit.emit + } + sub_menu.add_item(display_text, index) + sub_menu.set_item_tooltip(index, tooltip) + return index + + + +func make_menu(): + # No known way to "make floating" via code with new EditorDock. + # add_menu("Toggle Windowed", toggle_windowed, + # 'Toggle GUT in the dock or a floating window') + add_menu("Show/Hide GUT", show_gut, '') + + sub_menu.add_separator('Run') + add_menu("Run All", run_all, + "Run all tests") + add_menu("Run Script", run_script, + "Run the currently selected script") + add_menu("Run Inner Class", run_inner_class, + "Run the currently selected inner test class") + add_menu("Run Test", run_test, + "Run the currently selected test") + add_menu("Run At Cursor", run_at_cursor, + "Run the most specific of script/inner class/test based on cursor position") + add_menu("Rerun", rerun, "Rerun the last test(s) ran", ) + + sub_menu.add_separator() + add_menu("About", about, 'All about GUT') + + +func set_shortcut(menu_name, accel_or_input_key): + if(typeof(accel_or_input_key) == TYPE_INT): + sub_menu.set_item_accelerator(_menus[menu_name].index, accel_or_input_key) + elif(typeof(accel_or_input_key) == TYPE_OBJECT and accel_or_input_key is InputEventKey): + sub_menu.set_item_accelerator(_menus[menu_name].index, accel_or_input_key.get_keycode_with_modifiers()) + + +func disable_menu(menu_name, disabled): + sub_menu.set_item_disabled(_menus[menu_name].index, disabled) + + +func apply_gut_shortcuts(shortcut_dialog): + set_shortcut("show_gut", + shortcut_dialog.scbtn_panel.get_input_event()) + set_shortcut("run_all", + shortcut_dialog.scbtn_run_all.get_input_event()) + set_shortcut("run_script", + shortcut_dialog.scbtn_run_current_script.get_input_event()) + set_shortcut("run_inner_class", + shortcut_dialog.scbtn_run_current_inner.get_input_event()) + set_shortcut("run_test", + shortcut_dialog.scbtn_run_current_test.get_input_event()) + set_shortcut("run_at_cursor", + shortcut_dialog.scbtn_run_at_cursor.get_input_event()) + set_shortcut("rerun", + shortcut_dialog.scbtn_rerun.get_input_event()) + # set_shortcut("toggle_windowed", + # shortcut_dialog.scbtn_windowed.get_input_event()) diff --git a/addons/gut/gut_menu.gd.uid b/addons/gut/gut_menu.gd.uid new file mode 100644 index 0000000..53a33c7 --- /dev/null +++ b/addons/gut/gut_menu.gd.uid @@ -0,0 +1 @@ +uid://crhdyu6u7n8c4 diff --git a/addons/gut/gut_plugin.gd b/addons/gut/gut_plugin.gd new file mode 100644 index 0000000..01b2f41 --- /dev/null +++ b/addons/gut/gut_plugin.gd @@ -0,0 +1,157 @@ +@tool +extends EditorPlugin + +var VersionConversion = load("res://addons/gut/version_conversion.gd") +var MenuManager = load("res://addons/gut/gut_menu.gd") +var BottomPanelScene = preload('res://addons/gut/gui/GutBottomPanel.tscn') +var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') +var GutDock = load('res://addons/gut/gui/gut_dock.gd') +var UpdateRequiredDialog = load('res://addons/gut/gui/update_required.tscn') +var CheckForUpdateControl = load("res://addons/gut/gui/check_for_update.tscn") + +var _bottom_panel : Control = null +var _menu_mgr = null +var _gut_button = null +var _gut_window = null +var _dock_mode = 'none' +var _gut_dock = null +var _update_required = null +var _check_for_update = null + + +func _init(): + if(VersionConversion.error_if_not_all_classes_imported()): + return + + +# This checks the Remote file or Local file. This will not download the +# remote file. I don't want to delay startup for any reason. Downloading +# the remote file ocassionally is handled elsewhere. +func _should_continue_loading_gut(): + _check_for_update = CheckForUpdateControl.instantiate() + var to_return = true + + _update_required = UpdateRequiredDialog.instantiate() + get_tree().root.add_child(_update_required) + _update_required.set_check_for_update_control(_check_for_update) + + if(!_check_for_update.update_detector.is_gut_version_valid()): + _update_required.popup_centered() + await(_update_required.closed) + + if(!_update_required.should_continue): + to_return = false + + _update_required.remove_child(_check_for_update) + _update_required.queue_free() + + return to_return + + +func _enter_tree(): + if(!_version_conversion()): + return + + var should_continue = await _should_continue_loading_gut() + if(!should_continue): + print("GUT loading canceled. Restart editor to try loading again.") + return + + _bottom_panel = BottomPanelScene.instantiate() + gut_as_panel() + + # --------- + # I removed this delay because it was causing issues with the shortcut button. + # The shortcut button wouldn't work right until load_shortcuts is called., but + # the delay gave you 3 seconds to click it before they were loaded. This + # await came with the conversion to 4 and probably isn't needed anymore. + # I'm leaving it here becuase I don't know why it showed up to begin with + # and if it's needed, it will be pretty hard to debug without seeing this. + # + # This should be deleted after the next release or two if not needed. + # UPDATE: + # I added it back in when doing the window stuff. Starting in a window + # made it angry (don't remember how) until I added it back in. + await get_tree().create_timer(1).timeout + # --- + + # Kick off a download of the remote versions file if it's been more than + # some number of days since we've downloaded it. + _check_for_update.visible = false + _bottom_panel.add_child(_check_for_update) + var days_since = _check_for_update.update_detector.get_days_since_last_fetch() + if(days_since >= 1): + _check_for_update.update_detector.check_for_update_with_fetch(true) + + _bottom_panel.set_interface(get_editor_interface()) + _bottom_panel.set_plugin(self) + _bottom_panel.load_shortcuts() + + _menu_mgr = MenuManager.new() + _bottom_panel._ctrls.run_at_cursor.menu_manager = _menu_mgr + _bottom_panel.menu_manager = _menu_mgr + add_tool_submenu_item("GUT", _menu_mgr.sub_menu) + + GutEditorGlobals.gut_plugin = self + + +func _version_conversion(): + var EditorGlobals = load("res://addons/gut/gui/editor_globals.gd") + EditorGlobals.create_temp_directory() + + if(VersionConversion.error_if_not_all_classes_imported()): + return false + + VersionConversion.convert() + return true + + +func gut_as_panel(): + _gut_dock = GutDock.new() + + _gut_dock.title = "GUT" + + _gut_dock.default_slot = DOCK_SLOT_BOTTOM + _gut_dock.set_global(false); + _gut_dock.set_available_layouts(EditorDock.DOCK_LAYOUT_HORIZONTAL | EditorDock.DOCK_LAYOUT_FLOATING); + + add_dock(_gut_dock) + _gut_dock.add_bottom_panel(_bottom_panel) + _gut_dock.dock_shortcut = _bottom_panel.get_panel_shortcut() + + +func toggle_windowed(): + push_warning("You have to right click the GUT tab and choose 'floating'. I cannot do this from a menu anymore.") + + +func _exit_tree(): + remove_tool_menu_item("GUT") + _menu_mgr = null + GutEditorGlobals.user_prefs.save_it() + + if(_bottom_panel != null): + _bottom_panel.menu_manager = null + + if(_gut_dock != null): + remove_dock(_gut_dock) + _gut_dock.queue_free() + remove_tool_menu_item("GUT") # made by _menu_mgr + + _check_for_update.queue_free() + + +func show_output_panel(): + if(_gut_dock == null or !_gut_dock.is_inside_tree()): + return + + var panel = null + var kids = _gut_dock.get_parent().get_children() + var idx = 0 + + while(idx < kids.size() and panel == null): + if(kids[idx].name == 'Output'): + panel = kids[idx] + idx += 1 + + if(panel != null): + panel.make_visible() diff --git a/addons/gut/gut_plugin.gd.uid b/addons/gut/gut_plugin.gd.uid new file mode 100644 index 0000000..25811f9 --- /dev/null +++ b/addons/gut/gut_plugin.gd.uid @@ -0,0 +1 @@ +uid://7c5t72qt64mu diff --git a/addons/gut/gut_to_move.gd b/addons/gut/gut_to_move.gd new file mode 100644 index 0000000..925f281 --- /dev/null +++ b/addons/gut/gut_to_move.gd @@ -0,0 +1,92 @@ +# Temporary base script for gut.gd to hold the things to be remvoed and added +# to some utility somewhere. +extends Node + +# ------------------------------------------------------------------------------ +# deletes all files in a given directory +# ------------------------------------------------------------------------------ +func directory_delete_files(path): + var d = DirAccess.open(path) + + # SHORTCIRCUIT + if(d == null): + return + + # Traversing a directory is kinda odd. You have to start the process of listing + # the contents of a directory with list_dir_begin then use get_next until it + # returns an empty string. Then I guess you should end it. + d.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var thing = d.get_next() # could be a dir or a file or something else maybe? + var full_path = '' + while(thing != ''): + full_path = path + "/" + thing + # file_exists returns fasle for directories + if(d.file_exists(full_path)): + d.remove(full_path) + thing = d.get_next() + + d.list_dir_end() + +# ------------------------------------------------------------------------------ +# deletes the file at the specified path +# ------------------------------------------------------------------------------ +func file_delete(path): + var d = DirAccess.open(path.get_base_dir()) + if(d != null): + d.remove(path) + +# ------------------------------------------------------------------------------ +# Checks to see if the passed in file has any data in it. +# ------------------------------------------------------------------------------ +func is_file_empty(path): + var f = FileAccess.open(path, FileAccess.READ) + var result = FileAccess.get_open_error() + var empty = true + if(result == OK): + empty = f.get_length() == 0 + f = null + return empty + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_file_as_text(path): + return GutUtils.get_file_as_text(path) + +# ------------------------------------------------------------------------------ +# Creates an empty file at the specified path +# ------------------------------------------------------------------------------ +func file_touch(path): + FileAccess.open(path, FileAccess.WRITE) + +# ------------------------------------------------------------------------------ +# Simulate a number of frames by calling '_process' and '_physics_process' (if +# the methods exist) on an object and all of its descendents. The specified frame +# time, 'delta', will be passed to each simulated call. +# +# NOTE: Objects can disable their processing methods using 'set_process(false)' and +# 'set_physics_process(false)'. This is reflected in the 'Object' methods +# 'is_processing()' and 'is_physics_processing()', respectively. To make 'simulate' +# respect this status, for example if you are testing an object which toggles +# processing, pass 'check_is_processing' as 'true'. +# ------------------------------------------------------------------------------ +func simulate(obj, times, delta, check_is_processing: bool = false): + for _i in range(times): + if ( + obj.has_method("_process") + and ( + not check_is_processing + or obj.is_processing() + ) + ): + obj._process(delta) + if( + obj.has_method("_physics_process") + and ( + not check_is_processing + or obj.is_physics_processing() + ) + ): + obj._physics_process(delta) + + for kid in obj.get_children(): + simulate(kid, 1, delta, check_is_processing) diff --git a/addons/gut/gut_to_move.gd.uid b/addons/gut/gut_to_move.gd.uid new file mode 100644 index 0000000..a85c475 --- /dev/null +++ b/addons/gut/gut_to_move.gd.uid @@ -0,0 +1 @@ +uid://d05irbtlqxtm6 diff --git a/addons/gut/gut_tracked_error.gd b/addons/gut/gut_tracked_error.gd new file mode 100644 index 0000000..729a26f --- /dev/null +++ b/addons/gut/gut_tracked_error.gd @@ -0,0 +1,95 @@ +class_name GutTrackedError +## This contains all the information provided by Godot about an error. +## This is also used to represent a GUT error. See [Logger] for +## additional information about properties. Some properties are not populated +## for GUT errors. + +## This will be an [code]Array[ScriptBacktrace][/code] for engine/push errors. +## This will the result of [code]get_stack[/code] for GUT errors. +var backtrace = [] +## Usually the description +var code = GutUtils.NO_TEST +var rationale = GutUtils.NO_TEST +## [enum Logger.ErrorType] value or, for GUT errors, this will be [code skip-lint]GutUtils.GUT_ERROR_TYPE[/code]. +var error_type = -1 +var editor_notify = false + +## The full path to the file where the error occurred. +var file = GutUtils.NO_TEST +## The function name in [member file] where the error occurred. +var function = GutUtils.NO_TEST +## The line number in [member file] +var line = -1 + +## Used by GUT to flag errors as being handled. This is set by various asserts +## or can be set in a test. When set to [code]true[/code] GUT will ignore it +## when determining if an unexpected error occurred during the execution of the +## test. Setting this value prior to performing any of the error related +## asserts may have unexpected results. It is recommended you either set this +## manually or use the error asserts. +var handled = false + + +## _to_string that is not _to_string. +func to_s() -> String: + return str("CODE:", code, "\nTYPE:", error_type, "\nRATIONALE:", rationale, "\n", + file, '->', function, '@', line, "\n", + "handled: ", handled, "\n", + "gut type: ", get_error_type_name(),"\n", + "error_type: ", error_type, "\n", + "editor_notify:", editor_notify, "\n", + backtrace, "\n") + + +## Returns [code]true[/code] if the error is a push_error. +func is_push_error(): + return error_type != GutUtils.GUT_ERROR_TYPE and function == &"push_error" + + +## Returns [code]true[/code] if the error is an engine error. This includes +## all errors that pass through the [Logger] that do not originate from the +## [code]push_error[/code] function. +func is_engine_error(): + return error_type >= 0 and error_type != GutUtils.GUT_ERROR_TYPE and !is_push_error() and !is_push_warning() + + +func is_push_warning(): + return function == &"push_warning" + + +## Returns [code]true[/code] if the error is a GUT error. Some fields may not +## be populated for GUT errors. +func is_gut_error(): + return error_type == GutUtils.GUT_ERROR_TYPE + + +func contains_text(text): + return code.to_lower().find(text.to_lower()) != -1 or \ + rationale.to_lower().find(text.to_lower()) != -1 + + +## For display purposes only, the actual value returned may change over time. +## This returns a name for the error_type as far as this class is concerned. +## Use the various [code]is_[/code] methods to check if an error is a certain +## type. +func get_error_type_name(): + var to_return = "Unknown" + + if(is_gut_error()): + to_return = &"GUT" + elif(is_push_error()): + to_return = &"push_error" + elif(is_push_warning()): + to_return = &'push_warning' + elif(is_engine_error()): + to_return = str("engine-", error_type) + + return to_return + + +# this might not work in other languages, and feels falkey, but might be +# useful at some point. +# func is_assert(): +# return error_type == Logger.ERROR_TYPE_SCRIPT and \ +# (code.find("Assertion failed.") == 0 or \ +# code.find("Assertion failed:") == 0) \ No newline at end of file diff --git a/addons/gut/gut_tracked_error.gd.uid b/addons/gut/gut_tracked_error.gd.uid new file mode 100644 index 0000000..de081ab --- /dev/null +++ b/addons/gut/gut_tracked_error.gd.uid @@ -0,0 +1 @@ +uid://c1m2dbkoyf4fn diff --git a/addons/gut/gut_vscode_debugger.gd b/addons/gut/gut_vscode_debugger.gd new file mode 100644 index 0000000..03f04bd --- /dev/null +++ b/addons/gut/gut_vscode_debugger.gd @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------------ +# Entry point for using the debugger through VSCode. The gut-extension for +# VSCode launches this instead of gut_cmdln.gd when running tests through the +# debugger. +# +# This could become more complex overtime, but right now all we have to do is +# to make sure the console printer is enabled or you do not get any output. +# ------------------------------------------------------------------------------ +extends 'res://addons/gut/gut_cmdln.gd' + +func run_tests(runner): + runner.get_gut().get_logger().disable_printer('console', false) + runner.run_tests() + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2025 Tom "Butch" Wesley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################## diff --git a/addons/gut/gut_vscode_debugger.gd.uid b/addons/gut/gut_vscode_debugger.gd.uid new file mode 100644 index 0000000..a470c06 --- /dev/null +++ b/addons/gut/gut_vscode_debugger.gd.uid @@ -0,0 +1 @@ +uid://hflec26434u5 diff --git a/addons/gut/hook_script.gd b/addons/gut/hook_script.gd new file mode 100644 index 0000000..cbc9fd1 --- /dev/null +++ b/addons/gut/hook_script.gd @@ -0,0 +1,57 @@ +class_name GutHookScript +## This script is the base for custom scripts to be used in pre and post +## run hooks. +## +## GUT Wiki: [url=https://gut.readthedocs.io]https://gut.readthedocs.io[/url] +## [br][br] +## Creating a hook script requires that you:[br] +## - Inherit [code skip-lint]GutHookScript[/code][br] +## - Implement a [code skip-lint]run()[/code] method[br] +## - Configure the path in GUT (gutconfig and/or editor) as the approparite hook (pre or post).[br] +## +## See [wiki]Hooks[/wiki] + + +## Class responsible for generating xml. You could use this to generate XML +## yourself instead of using the built in GUT xml generation options. See +## [addons/gut/junit_xml_export.gd] +var JunitXmlExport = load('res://addons/gut/junit_xml_export.gd') + +## This is the instance of [GutMain] that is running the tests. You can get +## information about the run from this object. This is set by GUT when the +## script is instantiated. +var gut = null + +# the exit code to be used by gut_cmdln. See set method. +var _exit_code = null + +var _should_abort = false + +## Virtual method that will be called by GUT after instantiating this script. +## This is where you put all of your logic. +func run(): + gut.logger.error("Run method not overloaded. Create a 'run()' method in your hook script to run your code.") + +## Register inner classes from one or more scripts for doubling. +## Only worth calling from pre-run hook, not post-run. +func register_inner_classes(script: Script): + gut.get_doubler().inner_class_registry.register(script) + +## Set the exit code when running from the command line. If not set then the +## default exit code will be returned (0 when no tests fail, 1 when any tests +## fail). +func set_exit_code(code : int): + _exit_code = code + +## Returns the exit code set with [code skip-lint]set_exit_code[/code] +func get_exit_code(): + return _exit_code + +## Usable by pre-run script to cause the run to end AFTER the run() method +## finishes. GUT will quit and post-run script will not be ran. +func abort(): + _should_abort = true + +## Returns if [code skip-lint]abort[/code] was called. +func should_abort(): + return _should_abort diff --git a/addons/gut/hook_script.gd.uid b/addons/gut/hook_script.gd.uid new file mode 100644 index 0000000..a24103b --- /dev/null +++ b/addons/gut/hook_script.gd.uid @@ -0,0 +1 @@ +uid://1i8mr2knypot diff --git a/addons/gut/icon.png b/addons/gut/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9ad6a1dd9959cc5f88d593408c8adbdbd18e7639 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRdCT0c(hNQXTpBNYzcmjMvTp1V`mcCyz@(X5gcy=R=fq_BU)5S5Q;#RT)^P+_d6$6?svU+e>HF1b*h%2t% gnst?nqmheYk`Z^_GVkS*3=9kmp00i_>zopr0CvA5!2kdN literal 0 HcmV?d00001 diff --git a/addons/gut/icon.png.import b/addons/gut/icon.png.import new file mode 100644 index 0000000..e127d29 --- /dev/null +++ b/addons/gut/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvo0uao7deu0q" +path="res://.godot/imported/icon.png-91b084043b8aaf2f1c906e7b9fa92969.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/icon.png" +dest_files=["res://.godot/imported/icon.png-91b084043b8aaf2f1c906e7b9fa92969.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gut/images/Folder.svg b/addons/gut/images/Folder.svg new file mode 100644 index 0000000..00ff7a9 --- /dev/null +++ b/addons/gut/images/Folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/gut/images/Folder.svg.import b/addons/gut/images/Folder.svg.import new file mode 100644 index 0000000..2ac0e4a --- /dev/null +++ b/addons/gut/images/Folder.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhvl14hls3y2j" +path="res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gut/images/Folder.svg" +dest_files=["res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/gut/images/GutIconV2_base.png b/addons/gut/images/GutIconV2_base.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4caff60091cdddeb88188d0dbc6405f2846d15 GIT binary patch literal 135150 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∓`J^!a&b$&>R+^V*rF9iKSy z$lbuCsZaHECd))vP2Q4jUv|azQ8Ht z`!?&A$7bGe-|nEQ{N{p)pik|Ig%b^2CmOw1jca9^=KmrxN>o#KTT$?X9l4kD-_QTx zRQfyr)?Rsa`HR!}Z56F>c6}TNf)P@=e)s zDB;qo8V~h%OV;_!sFPp%IK#H>^S0@`FD>NMRu+nPWIV0@WzU-vY7U&dNmIoFIcIGP z)mNLT8sw)LM`b$2pF%t~%hY^iHhY>5+JS9LFANw4qaIgh={Gv-UCp1Nrk z6`tvRF`({Q?In5j-*q>?+lL%GcGp+JdchQZhC*RMU;QS}HIGsz3OrH>d!I1dr9yR{ zeE*qDFLSQ!8V)CuO;y#4-NZxZ{8qng=hXC>>!phXr`}qhRA>L2pJbz!S?pR7{gaI= z!pdef1-038xp87riw4TvOV=XL7xr zEYq}Nsn6rvY4Vpj_paub;?eEVPyWVV_UOU&<4XGNQl^FBlT)7`oE~=dBcFhi%<)UB ztb8Qzbc;H6c=45$JlvmNZ~r#n@T3n8yic<=qw8lZlD&OwZtuJxph=m46Yp0&ATpHtc#@C4>@VF4DCQ#qzJ3BXf zwuQ9Hx)j&a=0%^qW*ci=wtML;V7zO_66^InIj>pTr?s_w4w!sqUcdpRchlEaweUKG zYAm=l!SQC&)W$tw$6R!?4{;fDg|vN+)13bCWt!iyPU~5k772<^WPi{4c;o8j3Udy{ zzT|gPk1trZuodJLyU;(5UaLK(##IE(Kd!ZT!4lOc=RNdvpL0c}wW|wpMI1S5?6UBX zX{yTpsnM>w3k|1F<$R~@S-F1F_t;I9`tmi8*gl?;Y`%N;E5nTcT>_7El5#Iy(*0d? z-%ohaG}YPPy$a(q3xhNL&dm!w6S#4$;6k6LAI}P`OlVplIyaxQO)%(y(z^Mw$Kp~L zUvRTtQOM$K?~aN(#Im(3#7Z_K@~Gwu|2O~s-Z%Rs9_t zd9A-~eb8-LzCvu}mR&1b5(7{F*!Jq2xE8C$>$@>sdRh{~`)_yz9#D!C-n8JpY3ep+ zZ;yL^dw-~!aJk=VRlB>e(dk85+|l}b@%!8P?F)-GKjDyyzV-3-XY~(#0!}ig=RWUq z56-sMZknL8XZyae$Q4adjVD;0rb?dEp3JH%p=X>Iz4T9H$OerB!TvJ>kJnDIYSoy@ zr^8sv`QYpBRhKxNotjptZs`u$^*K(mAnxe&&+mRk?PFQHci_m6E`6s?awyN&vB0;iy~O- zX6kC`zFcJ^H*ssPY^vmKy~96r9_yP6Z9Bhf{;`kA-_y+RUu}?$&p+6($!Q-;%aVt- zTzgvt%(sNTE=xYP!z+wMF>}Sb{9~)U-@lmo-KEpzzpSURvzKJl^4oTIW3(0(-Tv}) z)m^=fKJ9teKd;yMI?rHjr_1)Rz%-SQCLR0>nNL4H^87&1+jW;V@-Dd4sCq`{^o_Dv z&Rne*-YqbCv`6UG-0*fGDK5WroF^Tey{+Hvd!P}#yLs-QpWf07E>`O`5(LkCMs^*VFe|{}?!zXl?HBcHI3H{C zOP!mn`h_#ZDMM)KCGMD&Pj!AInakHd+H>rL;-2faH&TBnC>}X^*@^HkvwQ8-<5B4`IuRW)|&}fap?gOUzuDq;5PLpnI zSj|~KyVyj#N#K#xzp9raHIcjL|J7CWJQE+O?6~5Rk^`sjqG=P~mh4{N(-E+ZE6VB+ z?@gt^14?o;&!!hAq>9!qNS0n$EX|zQK4ItFzT8yn^&wAp2MLDV_-pz4nv{wBn+M#E z9-k7#dE`}Eb~Hyso93tTYBx`?;M-FA;HmLGea97E`Z}4M+Ecd2?mD`%WLoOR1xw>s zT$0##Lpbz^fD>255o@PuUg1+@+(lG!m1RWyX4FMzh^}a{O7u=Cj9uydqHcxF(<9U5 zYVUBq_ZEw~ZT<1C5O4#jnkp9Q-uTDy1hTPwx8Ix)MgJ62|MCUM%<%?qp#zCm=(3)}}Q98YS(_TBRpv zx~y~l`tj3q>pxo_=~Nru>+jKtIDM3tL-Aee!KmYZG>-n8cEgM1fcEL~6tPu2+|ghIki1)`-@_^Dpl-%A=5;`karPq7cod4{- zePwZ|r@*#N-2s^{Vs{g})bbX)cQ}2^RmhfJyGT4*kt^azcha;#Loatpt0P*aN>iBU zh6mic7ok+X@OI(SFFQYp_ukl(zJI-3^}e5L>X#nW109}nWbTn$>ms6$eVg}V>MGmc z8(-|1|3YMyNmr;8i^1N9O;xfH$BI){ifmDN`p8JRKCEP&H@g_C&LhT)uLAgVl9Q%b zE%aQS?R9Nc^CoA`-rZYzp6$Lg>A6>~(f1e6k1oCaGfCjSU)r^+CXepr`#rj=*s^0@ zb(;T?V|RUXzIWzV=6P;STzG0l1gq(7&bf=Goy+a%j(c?{+-cW>rS^9MHvC-J;JU|b zvWVo|z_z>LhjN+5fzx@trM5`M9#3Lvh~2^yf02Uw2F0WEZ&q@rrx22}fvJXQ+WnR;pO1Pf7U90Jbkv7(4m& z3_z*ipSRCuyin`P@sVxgwl^zX`+f&@8G9sYS${Kq+-qFFB5=i|$A^qI&3%@; zY1fu7opEU#QM2xbPY7U(mg0&yGW${J7dQ3=r)sDAG8p~ZdoRFg^4#!EPMobi*QZ{X z_*^TweD05h+}%G`)?I&-aUlP^eOGy>z$42#(`=it{QQLZ4mF39B)+KRY_LVo6PJP>(_I(PM<7uvg+Sxl47dE)snC-sueBUG~trb73r`~pOw+wRPTzI~8 zY0#8OFSmJ>_q5;pJ1L3#V@++{hD82<1&ZNvDlHY$@1LF5;X3=q1Q?RD04!%~nPMw{Q%ZQLzz^3lFYGFmI#*KE36srrQ@ z%4ySUuhoyaZcW-Le{1b>?!6z)*ZRqRC~`PkHRaI3K0TXHc_kaXzl$m!={|C6Tf2Ck zQ+2xdZ>~p?b~iS#&p-dK^=aqr)>ChLx&xiUBNkq>iQ2@fyLo@OYDJ@Bi^ztK=gVf! z654v$XCwEm$?H!S#c_U3jeNAr=*5mp)6T?3o-F#%aJ`GswA!-Y-y`nF8Y%ynYMu9D zibp=%l$|x%vn9v+cg(l_^HjbyPm%4sJy$P+bM5`pB6A*0c&`})swQ4OteWzIqi4tR zkmq|ZDTg`wYb-JdUvWt*=%wiLGet3q#Zjk!)W53E#{H~R^@3(a?=LKC? zt9a!0ky|@%2J;`^_pJGieaG{Phnusm#C*0+@xHSzu0d$plS4w6STz?KP7nL^osD(v zm6OZb-fw<>zUkb1;pcl>n8XVAc1%^Ukt_*M6`AcLxcbXlj}>8GjMS$DG`;rfeeBhG z*sJ$J>6>0tP$t%Jwh8s$Xd?+a^sVku;`8U*Q|eFex_so_u`rvmGrF3K6Ej3sU*YsQ zlq5c_=gGUMedqNmW~xV|o}a%o+9!Fn6JvLU=N_gnJ6H3xaUJSWd9Su>vx-_l_-3Pp zCzRYy7DYKeecrg;{8Q^f>C?u{&sAr?E;xRsa8GiJzb3Wi*9A)mZZY8sJMpD9?R!|xJ!pBz_&{eu_~*Jy+))=E?Yi`GN6`Cimu8-^eOY|?Zf?YeGV^$;>71r(CWp-8FMH7c zIZV~(1&87hf2C{8?{i#EAN2dC`KCVheEsz|hc11toB2^as?=%fXv=#`JA`d+AST zigYYTXuX)$`z13E&kvlQe|R5js)qKd$KJwgH*%~#Slj%(zAm6OJ>ROcyjS2+^t?Ua zB4q57l=CbqEwr2euFuZQ$vi%v{r=Q62enNhK`QU=cSbL}9d~!z%G;f5j*CT}3bYE_ z_Bi9G-Phe`o=<;yXHws69T(S0-+JdCcjbB0&7m}L$!p(v-kgmdEzafZo!O^duinej z8ttU9K=ApL0KMpplF&|{)D%tk6({4Do^(`QqEZn4euvS6O-;MbH*=ePJnAX4m*ZMv zbwuP|e)IW8Lg3=PUMTx_-k)0>Ywq1U@UQjn`s|(m-}1iDTE`fob~-@n(9DVa-ZLg0 zKegku*IsUK*QD~j@7DR~Y4yHaJLT7@4eL8*{|5DWn3cksCMapIpW0WO+8z0-RcmWv zi&AyOf_qn4w{`_a6-9AY&Ocp$wQ2kQ8%M?4n?0__e*d?P?@!%7tFG`Cfv$+VxesFX zb&vnM#uwIM{pC%kbH7NzG%WKzol``u3O$UR=K~E0tyP zm_LzzEHB~og8d^`;i1X*0vwGGv&hQ0t;o84X7}pL%v=86bh)dTwOh9OY1x|@^2b2! zdzY0{r9R&Lx~uK?{d*1S>usm`Ybmyre17Arp7`hC>W`B*AGg}M{$ubN{^d8dR%B)z ztbM$1bwsJsX$iB^1=B2Z3<`gLNo>yAep*ayMvUJ!qb;@hI%@(-ZzLSrCv`}0X{+?I zHsNFEt2?(ZWIp|RZ%Tib?k0;RDwgY8OwVc0Ol+C(aF^HfNkO|eDJ(dbt7;u*@U(bE z&+g~7jeUB@CjKt?@bc9CjrXGVa@+r_Drs{2E~?lflg0ghhGk#(mJRu(ymMm(@7U(& zUYl_6OX!;?Ebpr3MBVJ0_|rPlPfPdR+Aq&%8wFmK%vj#F1zgSB1qdmV{0) z()GE}X5bXtH0`2Ic-80V)R(hT|9zgkpXI}uN4hnYe@q2RpPJ^b&p(#w{%tYnkm5|2 zH7}F30xw@UwD0)5jZEp+q$)lw*2>xS;+@V#o=?R|D@C_m;RuT=j$}+*YmuR|=EAG@ zA??yywe8Nwc%2x#ckF!rrf%Z&MBS)DfmwG$J;Zjk0aUTeS6wKhaaXq6xUztmR+s>Tj6z!_M(aX z`|or=_y1vd)$hDD5(Kwz zN;=v#pG#4yS!vP@$tA@eoZQYltt|qMy?I*7i7m5Y!lPuvSM7?}%X!!I2+uOhZfp5d zN9W&<;$K$sea?iZw+?>$l>58j-^FH)I6m#OnraK==xNyK<#R#@~L^+zaO!Q zo_}>l-t^H|QR|;hmChl>s=xVUj>w*0|75O8i$L?AKmXSr0@c~uJ978zT*an8_rv}R z+m4x)aD`s8+}OgG=(+!cijTlrqp6w8GM292W5F&Kuf-K{MA-ORMWbi`XRc#H`%nLm zn=xn4-5o2Io-0f}lDhP%d~M#El6b$pnf!LQgU*Yewrwsr&-0JJtmL8i`ZdQUUzg)< zc&F{eanX6r+nPVKl0R-158tVIe5vf;nPSP-7Z0RIzxw!KqWy&lTWr5h<2ac#^{Sta z?tN|xRl(h{i$bdw8l)Xk64}rZ?y4hsPFp!?%85-v3k@c`=h8ZL@#(Ih$#sew?5=Ok#yRF3-`_OsTCCj^zkeIs_gI^mpv>qYv=P*u{*!kv zs-$eE-sz>0wKA@n+&4Dt=G-s!XKqhu{yOQ?cIwkA@6Y}@mq&cs$65z=v4ss!gC^xN$WgV>Syd1E1zdQ59|Q3jddS;l|SyjzTYAI$W-sGCtfjYMnoQ&wwL|G;(hn8 z*@T@f`+fJ(k)r;LsgW0oF4=9|$;GO}6>-FwDeS!a^v|biHhRsPvDCV9_k`)sx!fl0 z&|V!SBrb8~;ejAMCZ-kpbCtJUH~*}vyiD-t|2K2X=g1tL`)&TMIcdsV3MvMYEgoz- z^LaT0UHppdZa*pbCd^UpVblrl6kCMr)ln6N>hW6oxmNe)hooQj#v zTP|g7wf_3gFj2i^#j17hUf!(SSF`0zN$B@8-+ao$?SKE7$MoTKWp}~NkJX^#=_gEZ zS##QdPEY*5&2Q4BuZeDcv88K9=-%0dAG>aw+Dl!ve|_uJ(XQRXr+2S4X?(pjLcLyd zVS}sN+K$vmdzxay!)7FfwLE-#tWW!~_ojRHi*>bjtv9-T|KHuLn5y5|5tq#mH21U| zD(&Cfo-ec28g!gFXXKGtrDx=R+*R&&KYKy^%$hFI*Je2fmlnqz{;=;eQ#k(tj}=_2 zOD-Hc)9l!yc~zkNc2f9^*BN=**@qq_`Z9L+NPX0L-r2TWtLZ~o;;wyJYGDmVPFLq> zvy{dtUg2cl8WSP)ZuObT@%QE&aNWMQ>+0Uwl^X=JJs9dMzQoR{zHtuJHdRp)e0_h} z`>y*odnAIq-iajj_Wxol_&6v0V*R;%9#NOg_Lp9D#w=Uf{BF^t54EwS7qz-#w1pRH zmK1trMvJV|NSGL?p1`U2p`(H=+Tg|eb(^H_=}qzC`Fd0*w0zoz_q#654Hd8w{Uvrq zXK8pyX+kAujPl*O%3$t!|8LH&sFyX~=ut2!=u!Riea)Yjy>$W=EJ|V<|NZTVEj-(t zKRtPab*G~CF7t=8zuWZh+g!(Now+_hXyu`^N4ZXSif+qTs-DrhwQubuM+b&z)nD&l zEM@Ly5|ivzx|UO@Z`RFN&px$q=7J4I-iaS(a=hKOZcCczmZ#gC8tT3DbVS*wHDAA{ z{PAzFzuAKKi;hNYDpTgSNIFvx^k~0)jr4u1S*gt}0zq1WuOB6UKcX*J!R;)v(Su>y z+V{uTq!t_ecxoRtr}tQ!^y&BqL9f?`Nxe|_VijEL7T&qwU$)BcNiTM%>GGR#MWjsW z3^!+ZA#bVwYUk>NyHiEq`LtLyO{>&g7;m|7(SJ{uVCxl+4}2<$-}&#W_TPjc5^{+YU(aRr*N4ez+-cR-cg&vu+5V93`90^G z`+t0LJfBwcT3`S1tn=^vLCGgcE$QzoZ}tbXx9{tDdUy7Y;12!od-e$Z-+lUMm(Y~l z@M)P1yYuwIO8j)}1AX@E>NQQ9`}EA-MGdZQdnd8}e)!fgCh}I&w2ot?tf%|5U;MlL z|Mir5PX(L1;qxBO(A&0d(LpE2{g3tTw%k7xeMBMR(Wz|hDuacKIj_(E$_Z*r3k3_l zK32ReDJb1oY|Wd8eu{lO>ZlLHx^(D8KrjLI97c_@2SKHb`fAQE29s0`ZW(2GgP&&*v$0|C?T~ zbRnxaF5^nxh2uX()b>c(>SgVl8!~(Cnu+}(b&@Z?-!dxCEX>!Gxif!G{?75&*Ct3UPFncEZ^`MhJxc^5uUy|h zKUjX|=gTj@e!AWI`rF*=rE%x8XJ1O+dD80OwEjIs?>;qsSB`Lt$h^=PnwjFw(fU=$ zDdPNYF6BLU50tXr?$f^Eo&LQrfweGBalz+R8~)y@jyCw`tzCHjw05OTn8E5Q^T&s7 z>Id#oTBx9Udg|Z2tsEN_Q)-{@u>JUU|DL9+duQ(q)|yejXCK#n>EFIQ^FCLx`7e^56L@ZKQ@zFR=x3r|j;?Xuf46=9pTE305;smUZ$4C!bTKOD`n{I> zbL#|-Y722OJA1xsIaIr8>hG2o<)pvgR6+BLDO`)|PYcw2?B8DSc#3vR z%F+$T4%|yl5`DN@WM>42M%2vKt^Lc49QiH1JNEn+1{F)35ucB4zM;gn_ipGsrErGu zX&1QD%0=f1Fkhdlx_xT%OU)h3-2dKbGuRpP-71djT=aU|f$Qs59=e`i*O|CM^w6?{ zihr&%=Rcfq_n63C2EnASySJszus-3x6o?dR8vlv`|gdrYV!enPc`-;^_mJBU!J?S#j`nt>&$^w26=+t z9~AhwD!=0PI}mVc3zzEqQ^rMJs=}wY8x~xf`pfg;pD!!7Evwh;5V4D%c4D=%{;gda z5yuriYpuV#^lkX&+YQlSkITc?9lqIK!()2&%4K)u{Pph+`@gT}HlK4glFjqSn%K{Z zm|W{yZ76(Dk1^)(XTn?_N-3@#TVZIKzwr*>!H}w-U^_6$Q>rU3%$w z!nA{vcCKsLtL)46?!jH>UEe3I`@6JAe&y9?8)BI1_ihr4|9hn`VEOv?FmwUmrl*C%00gOZ0&8e+d5~9t}Z<+ zy*=rvx;XQJF4K(Ng=d@8*FOlf{*1xnHx3#|HH7Fx68yoelki+o~mp+o9$zF{hroZ zwX%RcN~$8c7TQs5OS8=j)hBqIobx%~CSA*JU$XkY-v_r}+&**iwkgW`d++yt&aYYM zu_tKO+}$a9I&;gj)WuhC*gsR)TjOck$}PQZceJ18K4ITpeCjS&qh!QXwK&D)Ee6j+ zrhh+ikwG-^-qcmEPwl+;U*A4)_d1{TS1;w3zT7|SLj^ORWLV{S8RPX!PJe4HzbDGz zG4t)aX{}GU*la%ff9Foo9hs)*5%X%1yuJCfO1)_Tue7IZ+C1l1MQUL0ACr?uK2_~a*cqNy8Nqg2z;ZLc z*N64@MVZzsiME`uTF~-6r)gI1wa@DoSV%9N{8@`{o%W2s#-(wRXU$%G6<>b9n0MFn z*r$5#jSXQpzDw_4<9Jxp`{VAP?~BCNN_;-GNv`JaGI7ngi8ns{x%Bl8XXd=1cJ0Q^g4N&dMyMH! zu$3n*)R_~J7tOzqH|5>mpgqSH?{G5Veb;m<@z$11?G^#ye;xA|aKAko-`tTVrQ>-o zbjCVuY4?dCaVu_cT|c8eb&vM(x@n)+$^7I%Dn+=_7vnzj;cV&zzQ>e*4^ln-Nw!BUaU>F7VO$etpC2>ZgCK5}zej zewV#x(=s(!obf={)ssiIm~zjnneP7OMLSa%YvGTF?dur~9%@^#9`DXv;lB5?>q+k% zuDvb+?n^$2uV`Z1>0bEY!~t#3ueqx@LRtSVId68QMRWVps?+}-d++`6rct=?jos}{ zJPTd+>|ZCp-R7^Io_%3anPh&S=bgMM`L)k#YaR+u)~d3<^XrXKP39{;8#~W8edl?m z-~Fg{?%$W=8;o1mOfr}&ozz|CHAU&uN?*_3VoO zqyK5!9nZZxTp1tPSwD9d^Rl;wVy2;29xhIJd8>EE-!n#B;Y)OOyx+?IVP~`YBCn8V zKOMa~>-X&Iif0kBF8aK2-MpRp>)1X_-oL+5-*;|{?5) zc{|g3mPnU!aXj^`S)R`HCKh&2Z+|>(f8(I8H9OaNMf0S%_&i zd%I!z!>luviI$U|$E{bnIOSAl>g&(jpPw))PQKW=bXUy&_aCO^{CwcXu>W7eil5@y zVm2oq9Or7d-p=*7L%A=4G2u|@qpXr;tZNL8Y85ZkYO4%is1>pF^zNc}$qFeyb+#)d z?ff#4;oXi^jLYBbmp!({?1KaUiE9s4waHl-h8@J+2_>DWj_wx)PFcV z{?2|ScB{?*esTT#&HelH4As=qGig6=`RYF~X7@X?y!>ukA*0xj75NP1`~NULlmB+P zNpbDY4AFA+unUcqFN-&xQWjnFiXPm{YD6ONwN-)~d+=HC3QsLc;f`r15l zx8I@@BB5l>9dm9DpJ40u)!z!^Qf8Ge-L~!*>-9@Z12gt`Z98+}aH*=yw#VUeFK#a1 zv3JLw4I9p!Pu?lgr`oD?J#gyf^IPWx^t|DZ-0(du&XVDQZO!*XiZbS@);@PvPHucZ zYt5T`E%kbT1j6G!@#@t7y7#8&9?RsSC*ko{4DmJ@=@a-) zcNeWSx~+L`o0izd2Q%tfk3JXwyNYds{G*q(;RzShZx>v7vT??ZlQF#JX1sh4R~~)+ z;XqU9j%S~LTi**12)g~3JD*}SpcNgrRpz$nm<(l=kj=#RNDqc+D zOlj)lO+it)8I#2um2k*xnSKQrqRK5J2Q*vzS z(f6xOYndya-7T%ix#%A8b!T>?vzuGZvqMwoXN2za(|LYz>q+bDf~)8Jnr*$&t?pKF zQmgc*y#4D1Tpx+bMoe9lTp%7I<}P3Rg^NLWUsrIU44<>@E8e54E*f24aE&{9hn3rm zQr3#}$W-xNtQ)GgzD!B@*}if0w~Mv^YX9iR9lt#xDCy;;r4jpTEccaN=KgU-@AiWO z{iO-lH+FXYJndWlUm-5yR`2s^YYvNA8gq)>(-1J4bz+BwSnZAW*kfvLU*G?_=Ptdm z_*eN8LtnSwhI1d$GI4s-qSU69&)<=j^fK4EG*iekXHTu|uKHbU3}JhIJn9xc{_gL} zRi@nYmUM(N1{_#rkalLqMy3NS3>vdK4Kn6Uy_)dtlGS=2Gr@{w!jmQ0HurLgM@Z(h z9b9C#b|yQo%bG(6%I-gJeeHdxx$=j!+@Y#3i+1mpK0Qs>IX^%Do2q)IJE6| zYHThz%B{c0U}E}_Q;n)EQ;+L1?BiQ-b7zmio`P>0bHiVWTUEc)7Q9mUbY`h)?X}OE z{465PH}7s+aBNzLR=v&Vgr4kLUVEF;{E%Lw>#0Fre;ze%FOcty@uCsSW95mYKR#!EZ!)mSk5}$z5xf@RNcAsMZefj&gB~1&M zS2I~Yn~@yGFyn)J%-o`cR;$Yks_pOY`xhS-Sa(0`WsPP@{Hs0lOkOUIR$8(9&WX~e z&kuci9lz&q4|n$5HwP~*_1<{gJiqA9($@NuXFt5%zo$ob_pFVVXQaK^;cHy>BFoL& z`Q?`X3zr;Wx1GJA=u)Zl$A7nc zE8Z`z(}}&_e_PK=aI%`O)7foXJ?7l*ye2vKgYeRv9qSIwTXBX|Qq_QvEis*K!$x7pb8NK~=w z`WO4%G4}gEwWk~GTg&)3I{5nM!nM2^xocdroq40AvnOP%a9!khe(%;&i%#Cz+mjAY zyRvmp`DcM^-}#rVdoFF3TzcMNVzSQRZ@2UL8CKlgD#zLVXsWK`>6cf1e{0;;cPXu3 za%HuN-x-n5O_dIwBe>t%Pgmg>&lR4(O=^?hi^v+q8EZ?+sp*O>I+i!B` ze0QI$@KZSQXV$x~?TYGte}CS;c+GOzi&=GtE$46ebno7u`TD#2baK1GpV~PmX3mMw zdVk0F$)|Y>1N?7%65qNfO+6&PJ89>>6Hg786;~=~B!nu=Wd2uZ)1Pdcu5_j{jP0EE z=I)s%Px{aLex)Gh>Du?o8J1s*&m7VcSpDg?iP73ir{}t?*^{dDIqviJ>GzMHZw=L{ z+SwIi*SF@|w=$dWhbM2gj4)$An8*B>M~gZ7UW<^>Ki@Z?I6RkV{rGUTc+$0=yMgi1 zv0?iUKL`-|Z~a|1rs&Q*n|y(!hjSKG+lK!z<%j|G{4ZC@94n{huVLc zW@+cN-#_EH8_uSm@@9uuxj>${$Nob+!vQ5hdSwny__4dGH&tx&Zl>`N1XFo^n7{tr+1l*4Nse=I*XP& zx*AEZjFb*yulp7ME%8^1qQRwUnG%`hua!6b3Qe?EjQJ(@^?dyC%Ds<7ySEFzwZHy) z_UY>HcirCY5|I1b_1j|W?rkge#NUa>EU$~Im6@l_@7D7Fz|+S!q-8T6PxR+Fxm)t@ z*Xj0s8*_Vq9Dlt!>Hoa>5nIF_-;&ad6AWSu>0j^oc#Bx-!DEqAwET95Mn2mwvu<(x ze8KFGwk`iXZvFhX;K6p2a`bOW| z|2lQtw22Nlpd@;r_WRxPPJu<~DrfdabO`w+O$`v<($eSgRM|%}KFY>*UBDDo& z=b375SPSg_zH?Q%@xJ=+Ofs8pOC101$02E(a_rgG-Kk7>{(7_DJ*sWr$Q@7`Tge%I zv}wxjo@Ax(D>`D{HHCB3&$%YF^3PM7`h%x@Z+sG8+j9O_9#hbcL!o9(cRijQ>NvRY z_siw;|M8udn#%6Yq5X$RGe)maZD#no>5gv`%J$w_{o_qY|NT??kF|D3om&|tHI2XO zS1Vhtd+J6HnrQ_D>7-;{1A4`&>{5!+HT%wXl%Q!c}X< zOF0ui8ZowQ54RH8pd}ahq))TznRdv_w#~PNR(NTYcz!=U<#+Ve6FZnb*@>4Mo>q9S zb@|ywnY!9g7VWzQWfM>Fa9j6nT)dch@7}#OUpN&bi(1!k_ut|bXZrDY@%u+h{%-5G zk$-*o9^;+Qr@ZEv=T2Yr-JL^n@0P7wQ$Me733?@(u6bog^akTM{9pP?jW>p{tUgp{ zX1sCFHE#Rgr>e!Ax+13Xcynk^@!57o_z`Q)!3Eix^8~a~>kHmJpP2JbLLRmjp!%t> z)Y6D;?U#0L*|dMf>FVIhWY*W(Q>B?6K8sm8efQU|G9oPdX0-fHlv7Y&Ed2JSZW*r{;!kbo<96i?;r_xR={--`uJbo_E5H;t2#rMo;bW|a_7@);)1D3 zoJX{OYkf`s8w`Oj>(u~G{>zPx( z*Kx^)UwY3hS-IlHj-s6J1<9gkC0^XRf0I`<^X7t7_iHCDnDi4FuPs*pbd!Tq@u_Cg z*}0~?_pJ+!_D!#=`&yg-puto2Mz)jow)YuaC7F>M_sMgI=N&kGde_Ih!rzK&`CBGj z^ndrJdF$@0iiV4WPcMI>7j{!{y<4~R=@RWr38$A9B>kK!UNx1!?~jOEYI~V%_{^t2 zPq$5UdGBxkh;#9wg$)w7_%M!QFa#ZpfZ3oVC)4eC!f<CKPIPM44P&+Awp^Cdaw z`n8VS+tP;n)-w8*nr5t8?^Nq+%8+pSf`M7C=c4)o4*73<51Qwljkvm7(thU7_0O{f zowV{|_!Mk^KS;UY zq?cP~KaI0o|9IaWi{D(48@K=7cwBCGOM1_#7N)RP<(BxlJJ$t>x-99s(WuI!*R(2c z^_6qF5#eVGpI(0#(ezyS)VdI*7b-c7edpF#EdKTG*6rJt(u#^lIJo><1fng@+G*6d z3dWjG+itUH+Var8yfq2tV$ZU@S~sq|E>|#b=7pX0E9XYEzW$+au5a!W%Im3F`DjIg z#G|-_DQ`~sKVs!c5;*tprC++&{Dji$lS_J>Z09{L%lgj#{YljyHk5tuURGwX zTjLs2Gmr5f{U332mz+LY^`!9G|F>`7dZ-B52!gh;FG+dwES^*O=h3XuT3-i_E ze(YN5b0@s+_0bEhPru5QUJrkneres)9Zj3}Xu$Z% zTbVI8;^=9kr_C=JzVH9g6Syu~b3ZM&zpV(Ai^~}i5 z4i;Ow=T=kSwJXLeuiTALOKn-TD0HF860IOk-^!D3>?QUpFH{UW=vBFJx&f<8eAJ73 z=F3a}{`vV?|JlvOMGG5TgC2#hc&EL;)ce7qqSKo$l}0h9B~44;?LPZ>(ZWmdQ`|O8 z3NwE&zxe4_i`_n6TefdcesrW$WFv=Si-3^k`M6S{ZTIVb=lq-cPD3ZlT+GB})rPwf zRed`4nVLPb`C>cI9BEyu6~UPs=$+9KkeD9d`-OFmulSNn3mQTn_OR^S-=WC!Oham$ zH_PGO3N2+H4F$KZnZoyLI`1ys@4TtyvUO7gjeb1-yHVU~qLhKrnYo}85I9--UYzYd zw%+z_5faL`C2zJ)>`m3ndbxXJ%d@|YAO48nsZBlIDte?dRVi!d`uE%9oC=c|g=Wj$ zb71UdNa(0OYAp1mPTj7T`C^#JlQ@kvEPG_ys?HhSsF<+f+XBb8Gwtjg-!@LO)@$Cc zy2@b1-G@7;{#Z5P6wBQBq?F(4k3BpXz9-3DJ1IAh0~7{9LaUBnRVXY=_xm^X`U$f? zIXcUAxz>gUmM6PPzLL8upWo`}{jekS`SwuWD*ZR=YufgBAHRN;x4xXOLf%biZ+QKY z$*hbH>|%_0&Q>REt50$2F0pOfUz^i(%l1~uw8Iw)MW3ZB`~G6p7A}!t5?PU~{`f@B z4C~DsKz0j>g2F#4I+~GT>+b7|zuq`^!DY&aG%b_pXWL^$Y>Kk@pPktB@YJRM%RhG| z?%W`dbYk}^=hHv8?%6*1?|y;5P93)^*jDCfB!pTNKRYvXDrb>=sN&mQmSK5|CYQZj z;3U8P@9S;zPH`MwyFTE5$QuRQ+e_Z5r8NAz^xk%f+{Pa8t*wC^oJg+d7 z*PFxhW2>{oI@$2{sncI6@@{(^X*J^}zufI@VP-Glj!(b;=)(F9Di_kZcX=F*Gdj_> zG+tahJnUT6?~{8s$bicPMo@}S30igh`htfOo(UZLoRyn({_Ej;Z>I_U3YC2!9?-UT z{?}uN&qa&uTO2dh_F+uii*T!mJ-Y8NIsMj^6B|+urWvJ*&NlW5bzKsW*Oft=SN_!N_syB**36^~-uDh`Ts(D7Lt` zrrinLUCk_X?KFFH&&QU7(++E?hUhA{q{|z-Purh=s_(Ve_7j0;jl=e+H@EM3ea0^1 za;NIVBhQYTr@W8kT-e~c=?`c5g7_RRjewgvjA!PQE9)p;`87B6G~3>5*JJW_ooPCz z(QD72$1DT$nljRHx04WAiIeoOVc-uzp1 z?o{U`55?!-Lp7yK-aZap|2%Bf^G$a$HwJE9!NNrX96 zAMjrHZrhY}=`U5j{a>f7@4cwH$$RVSjEhlONz-myaWzfxnz`W1O*g;abw&Oq3v{hl zEa3)S+Nqd0;q|^%a^cNw{^ss~_?o5&&*}`-`unGKUBbb4j!t(M7pBb=)j#xkyIEo4 z%GN#4x6Piy@0475?)BRG4+q)PKk9&7lgaD4BK}5F;{7ke^Guj-ectwTbKJA??&2-z0{CxNLczJ&g z&|R_{{nq_nRrbAVi`?}{gBzMpC2p^uv*AN!=ApY+Wg?wEnzw$m6TfA9_4utzABs=K zMr*y5+&rPYbo#W}tF_8!-#g73Vde2@A#?YWk0xp!jyz{3$CRd%f+FPW0H_|NGR+TKhMh=e9t?M1{xb;oGZ^|K17TRsXX4u7=FR z&Ty?hogG`8E%jtSd~n2y4c~Shy zJzwYC@}K6Oq_mBdK}CquruHePjV37T9+5i#=Yh+XZQGbUcCHsXUVhB2N$8es({%A$ zyyf4;*ZDmA&K+g8Lc8!~-qkHfE<|5pI;5dnlGps6r^VJjZt~u6Rh9%!MM*{JCwc9}Vtb#Hz2Y5juB}G5dN19WI`LTVPuZ`h*$~+mY(MRx;@wUPVCkx0TTcJnc10s)%9q7xV!$u z?vPu-M-|`iTH30{_`daZ3ez3NJLfiYFupjTl$myZ-rrnBm+b^YgywIc|~t5Vyl^msiH$+V6M2=gza`aA54V z_+S#pW-z1ppP=E7^omK>=Ve~VuNGVX;ZTwNW5FKx8@ob}Pi=}m>wCNHHlKg(|B87Mpt>ySjIq6ipsJKL1;MPRvh=i8=iOFNZLYiP)1vj6yKYA_|NH$9MC~@tDpVDf z+r8%XkAUNaOSMvZZBA$_=VWIWOXLWFit-hcXV-?>u4DT;KdVS~^-9Z?O=o(N%l~Li zsotfpyH=U|g0Auw;r;9WpMMg1MK+@0Tu%#!>I2*Qy>mV}K8puu4juFRHJ`T|Sw=WT zPPt^c_Su{^?>Hk3rfEFKPUMD~7;IFH*x4D#c-(bapZ1ON7}2(!^P3hIF!8y!FDU>W zxFx{7c*~asi>qpQh0;qGeLdng|Ka422%&c#MmjlMQCX+|w&==+PwO-0mO9#y^}ocM z_wT)xyNbQnZn=;a7Itv?u6OsU->3c)J?X^QZ4or3S=2Ic8V_40f7!{t6oo};E1N{W zhtB9*y}&6naQElmR<3fboj3RxE?l~F$jAxg{~5i@*V{05x-c;WY_Db(QZKdYyHRo{ zEU{|!q8ppmUfj5D#nk(0^CnN--TI?t_wK#ge;dkKG<2_Ievb=J`rIuYY3Cucu)$Sk zr`+~rZZpd{mra_lJ8Mf`i9No)|K;zCJ7lIWNeXynmM%T#@O8lxJM|tv*fB}#wTFl1 z#cS85$yR`);KhV!JB?aajmt)d4jkS*!BzC`y`_8=?e6lMjShX{sBmwP(K)b;TYKi@ zdRGrckqsXIUOIPJw+Q@oO>J@Wnv!*LLFbO-NwulB^t?W|i(EDA-k7j7LMg)F&O}Bz z>%8EDTc_5hTr7OL^_-n$WCW-{R&31MaqQ}Xhw-zn9?ydq9;D&$ig$!>TPT9_H8Zd)Myw`+i5;%$^XVks$ayc#c4Y z+a9J4$Gt(0)3~z)4#n@vVc$~C({d&zOmGUHSMBkspUZ;&XJ=<0{_*kg$0y(W9(RHZ znSC~2|32P$x9s-QDg0Kv8?Ss?pEE^p+LJh?mVfUaO^v$$N7a$}-ojF5k+2iH-xic> zoR5*{)9e*C&awt1&z#H!^_CY{rp@iPCG-l3h3Rw*^x zOjq2sp2@DPB2jWnNkCQmMr-cSxJP0CHfi3FmCZ;N16`ZzwB^LveXFEEb+VZ#@1dry z$T~gUsbQI0ST`%KJ@`Vt%6RVY^{>PIRw%_ReB*SlxotPI8h>;{%!ON1W7w+m4%Vt3=F7OQ!Oo);GV@8CsGhZ``(u_$ z3rKr;k^P&=LRTMWO|aO^+!?&*H2c>>cXFR3d!GH*=6rj?MZuj{($q`xE^Dis2(LQx zH_Yr5Pj2gO?$)|{O<{gN6YLvySEwnr1{_dwy;JeH_r8eJ8u5kGIK<`{^qykP{jzaq z+>z-O`Ukn*HR0*ycUx z_cg*Yk173yg-x2^8A`+97}QlYgWN44e}Iq!eFYP#^OX+p22@&C#XTz@9uy#KTJ z2bh|si9WN5D}O$>yl!&3NDv3B&ZCo?D_9SNt(eZy)!;PUab-fv(IaRd^=&*ZBP4*?CjHg9-uz0)0`8!@;XxcSPK#lF1WjPeXRa8 zoyq=czb2F)SU<&p@Ar`-wX7?ir>>kX$|N*9{{D$ij*mD&R+i3aUKnD!@ZU*;RY8tZ zen=_!>OJ|jnR}P{@%=FYSzo?pT|c?uQ%Ry_j=NS$q9CYn`O)|3($nUzH{YyQI5f57 z=gWP4b)E}Py}0~e^w6x07yj}o%#q~Uo2Pf?H0RX!3wjpaYIR(&`)@+Ig2#?(qq+62 z9pWMzI;6`PZ=IU@pEDr(Oi)SW${gh%1ulg%E6uM@Exi-Iv;Xwkmlu= zOZ1&|n~5PRD(b@#(c_=qIDjr}Rhswf-^Yx+JhlmX@;15^KNrm7KD}j0!1ej5=_0XO zKF;_4o{~5_Co=D5WTM5^k|&0f6Uz4%lwaSg?y_H?>1X&6L9U1+6Q%QZJe2-v8Nrvg zN+Gpr*U~13KTbD$^>1F^|uvXMy`tH zkBfStlVy2id-^H%>t|;gw?myWJ%sPtg3m9F3cgK!8g$U2BjVDhV|(TV$2Cn|KY``! z@k^UdpPCnts^?X?d#lmuWeMe5PKPn9%;8Q0$B4>w7p2B)*RBbiW?#6quX=ZR{Et10 z{N}&#<_i7I`E>1y^u&Xk`{x!pB?|mm7uAxnc*}*DE0ZSN_;ZYHo=1ni$c7I6SC$dW zQk!0uhPvup{^oIH?SedQ{qIxDXV3O{y3@Az__kJ^t!0w4G?qTPc1p+ERjyUIz{2Xn zthp0Ed_3L%@FciUs`ylWJk^_hHK-e%#vYY^OHY2vt=BJh?8rGAu|g|wYPjdZ>sAMw z?-f^9P8V+5E8f(yi`nrM*JO`H&)c&l9{ANPY;fIk;#tDW`EEW()-2e}^(%hX8>P4L zwWddO!Vj+6F#o;PH*c1!v0)F{HdJ>8?c!S(TqvS;l9S=ZA*B`prxP4&*RGBH4$6rh zHOp2zXcP(c&sn0fwJve@hBwm$wVo%nsGJh9+Z-A6qKcj6YmpPnytx4^f6_mBZ9C0X zEccyx$Lw^`M{KM*k50}1_hoqw-?hz;LLGS61o}-ruhS_tcD6dNqfvU@xZ>R}lkNEz zE*&g7xr<9Jq%|4F79)BG|Qp4xpySDQt}Z2wln+4Wzq@BjBz+Ge$YxkiGZ>bk=+ zJd5Rj?hH5378dFH>!a@(IF-w7MrrGX&FfOuY+Cn*GxS+%>!pWZi((@n#gWIdngFlP zTYIG@{KyISc=)#J^pZwy@1=VJQh!L_pHTkPT5QUuB^$mxZHaq-%q=pp?!QXg#oDXw zHZuiOH4+5v-?14nMjZOtvaD1tlqZ>8XFAuZ3G1gXJ-6%AslAs@Zoi}>_@yxL=dLwg ziXoo=Q}4ZZbn@zSQEDuFd@S$?Cwi`esS=&gpz!{dLzqtra=_FNbAob$9qTSIyXkr+gi(L*;rO8}hz2`g;A^ zvu9~G-|kA_7WYr?pT^lP4;S!y~6IT{&G|fzx>qVjm$xj5xTm%b0&g%_A3p9I$gGvY{*;B zreb%0sgBiDEphHHrWv%Qw06n)Uy8fdED~6? zXWHA|*v)!6pb8^IY-XX))f-t?>K6%GJ(?1ysW@f(>TPG1ZCLvHP}8BPti`Vm$puy4 zP2e~zXH^+tbt%wtsz}w7_`8RVxj!~DiG6&!>-D6O%8qCkFFf_O_UWz-Qqt0qZXO(pEg>BKF@-s1%$q@e-Y2l^ zx0mVrRabUK>@u46`o-e7OgsMU6!WR!}WQRQ>TiV{=FHMyS`0}&f7U5 zUHlO{tIi_VILU>pIz+u9cHdI<-#g!N<$`O!Ugc?LbcVbOR=a)j(5H8==hiRP>pOZi zb*b8urD-a6(q`vsB!TM7#dD79+p{Gf?{ig}*f7P&_S}Z&`?NwU7wUNGO}n*vX7Bg6 zCylN2G%mk=!4>X!bk&PfoS)-$zwcL>^kU~L=V$SPx*7?Bq3;-jPKDm%w0JY2^e*@G ztjGmLH|O6jGikoLG;j9xp5{yCwbD-0@-3_%sm0wCUA3gCJ7?1!QQ3^MT@jF|m%OVY z#Hk|0$*}Unw_nY(*K`G6*V4+5Ic7YGkZj*P z+hXA%q4ffLD_A+_v@I^2VJ;Bx+P|N5d+5B1)XZa!D+9SQl?5sby>(T>NCtcB& zqYUNZAH5E@iu&xCB-kCk>B*zew3jJ6xveJ5nz&)(M#28LGL!RSpt?#!>3Z!{E!muw zoi{}K4v5A+sGiZMQ@_xA>k)$o4)+7*Y`ko|X49cN_u}F-Z!c{*^wM@ox;#sP{&@oz zQLYG=nRkqwE>GSny3p%nrmASsI5_}MSalYi-Y`Yz{NvIkMH`&<7gRI zI6U*?&d8Mubhls45Lo}{#j#65cNyMHElb=89#fQz-LhrNf~8AY9ZuDTmQUFJ+skzW zSNH?2prVZ96IG|yggFLn-czr<-OSkWRF3Y#oiYKs+Fx#W%})r8Qkf(Xv1_8bSP~1! z=1mJ1F1&Z2@9rh@8E<5#1hsu==UjO`Wm56e>D8ib6|CRuo~p&2oKU=S$vS84-Q90F zqM}|eiMJMgVW%to;@*V|4L_&L9W`cCY!L`L1_}wwgKtee=3MN&w!Wm*Zg;{$T4Npf; zDbnAlU;mLSDkny-~{-SR)oqjN+FG|ZTpdGNpihVv4j*&)fvulKE*9n*g# z-1@S3{ltGW|NA&OJ+Uc{Ubef}Ag(y2;nmhNuTB44lguoCCH#r;^ z*d8vt@Q<9m(DX(7_Q_p6d-X- zHAk96&yFb@mNZRURx$O;x&0}ywE&S-%WDL_KNKuk%r9-0bKuw4*M@JG7gs(44RxRD z@%a|&?ajUF_|*kp&-iVbx6k*SZ^*OdA7aI}|Jycg=Jwrxb>i0l*!bUned(Xud*;vn z#Mgea>$T|p@w-Z1zOQTp zjVGjdXdb^?|Nrl5`;VRe3s$e@W+-?%HT+SVbl!naPfsWA-V{(*x_G}qs7ej1$ofy) z+&`Z3s|)31S<7*B^_flb56vq)mmRs&D{USGZrhv+*mEmwcJ75smzWe;q?T__IFql| zrY1MlX+u!#>2({H>V>WB4*vHjEa2j)wi8cP-6}gnvUW!Nn;D@c)Ue9nse5d!>@vT( zT{Gv;)$F(U#bKFw+f!Po)8+fF(%1hsq@JD@??1y}VSl=05|cMaXJ>^fLy6x^BUSA< z#si_@v8}7u?K)LntN5if_~Uc2t>3>rwc~yynxq{!Rc(o_k9x=n)poV@=?iQ1cXCfl zu-tXA{n@j$oO^p_s`EKmG|GBo^wq%ZrcA{ z@7}&z+VaY^^PFqbQSZar+9kiM%)qcCl9vb@aENg;NndX1K_&3)?<~>r1v+UNmXt~;H zUZO#edh{XyZ~lN&~%+1Y>FU%Ei;?NYHHGB5OJ9l^(R@~h!*J)hbeEaX6z%rTO zhu`>xUcK9Q#^%M{+UT9HEA8D5uHLcVyHq?Z@%|yxL!IsaJ9pSjTQsGwf^|W_9;Sw= z3}T;noHY_Ao)C1(nHprLyflUu#v#-FACxdnU%VNY_|keMqZd_M_09 z=d2{AE?9c`-6zSX(}b>Fzy5gQdzsF|_1^^iK{?06r}E3D#hV2suO?>))F(`C4p|u8 zwOm`{E6_oZsc&+o?KOZQy+v+_rr&bwx%-Qha-gpG9LbRS2y zvHZ>xOsdM?l zUe-S6oE15Vp^ovvZl?~F^JYgxtTuQEu(b;0Gu+@>khkS;QQ|cHu>ZMDLHl)&@9*EF zFRF9P&!%Wy&Qz{l@4sJpy!Xkuh^EvRZt}Bc&5G~>HRX;>`SakL)D`&~9cNPSw*_my z*>vbb^Sx3pmbuezYHNpR8isUs=!?Y%yxiIa-)VSp#dD>GQzW@7I_vOnM@b%3tM>>8^mup?O zPS3>Lyq{(J{L4$@pH*f}>5@Vd$;$ZsSZ$tO&br-Jwd2sGjZS}JKt!wTG6+5o3@nW_8 z#bn`o?tpYePu`bHpsVUzj!am#Y}v#I-!yqW=3MSP^EBpYV~EytF{jfvxrCzfCrwX% zb#Bs+tv@VVSI)T4#8@G9An6^K6Jz(3S)~kdOa-e1ls0BwUbfb^*EclpUSQJqq|8Sz zvg#vri|gMmdw2wp!u;vCuTPl za5wzE^j}>?Kt$5x0=I)`gCxT`#tpweeE5)Hx@vV`Z`8ZFGi2W=wq08P>&fY(i_U3i zi1H{!Sla$GtZSaWK_JL0=hha^y?gh9mn2LOGWvgM@r8>QC!XDxWnk#kzIt zjv8!Z64{XS-Ldt>t6Se*{+;?u*ZxH%v*M|F7E6WCn_g zYEte^Cv#I!D}+l*TKeLZD=Z8veX`*_ zm8Z?kzgLMLh;`!NtLPMsaPr8BX}HBOhxtJgLvnk2Tl=cjtFwQ;Rk|tc%ki|kdznp` z)%L0BYSaEdw*J8I?r_bQ1^l3F{QC9bFZ0ho7H<`B;^^#f-&dS+4LncMX>4_|%u)PG z`ho7cnP(;0@`dZUUhiv&*PE0mao{t9opMXW$-9ng?t~YK3vi3C%3_dbFEDLzX1KEC zf5+kX;uozISp|08n`d+XK(1(S!qYE5?4NF}5L`L4#XR?x3%KRk+0j#R^~x0%MHZ?4 zoP#s&`?Iukt$1=>?%Jk=zxm?kY?7}(dprBQVEs_dFr8a*TT!`y^oEuRHw0wcmQLwr zu;F{az>pigJ#Xq()2&M99lSbP87Iv?vGw<%Ooj&Y?^P;doB6El|tg(}X}n0D=>vts+dfNtLpjEYrIZBB@)cObh|x;qC4EmQC^opw%@>{WZ@$u3W$F?A7^d zqp8Q7dyUsV@4hrWefj)6-JRcGYQ3JJYy7r;`3}xL!SA@1Tbw|XV|FfO(j%Hq%oW7ut(BGH8 z4{g08dPGfRLxzn8Y@E@;6LhGUYWx$E#VdwCWCfJPAMJB>4KRGBamb(C3ZYxV`&!Y><+Yx^U@| z6FA{)JfnQyQfTekwaV{S*UH=pms1M6q1v`VA?dDe-bWc>K_SI##y!d{9!22-vjgH3 z%p_ShIB;xaSUJhb$;;_sL+<M6&tKJdJt=jG+| z@IdaVMT=uxwf=8%+JAhRQ&`s1nMG`UhfTID77g<10u2aus2V*AO-@#3SWuP28|C-@ zLs`75Lefhyp(@?o?sr~UvMH$~?P9c<^3Pr|Y+-|I&Sut?GHb5h5nXacg!jP|S1pP2 zdMk8Omi}IvrfS0@o54CkOMT7SwNKkYwIR!bef#XR<3f8^x}BDk&Dgu(a{P`{e`i+r zw(AOVUvtqpzmZ>2kbghp9{vZM)A%PDENBSTlrIT2kuUw_vQ^?hF~hc^^OGmVyB4z* z{noLwnsk%3n0WAYmoPA78{G)ai1q_Nnx(xMfOWiiXJ& z2Tn7v?a=nn*u_@vn5xjy@#!hs1TE#5X**A;O)i-*>C(jC;wNkG+q&tppFXvwnM+Tn z{q5Vf{?yMKAWeoBlQm_paOLV73%-8;XnkL@tdrM?D2DS(Hc#Z-$j7?I>*oUHl7yxH z5#Ee8-B)x(R$hOe*>i+tZ9`e)n-x-$*H@*-tehf{78~)>W=+`!a7Xsy(}&)DC;okO zx8Ab%jEjy_lIan)IG0r|6DAat@V;x}`nh1W(*G5^8TT+O(BJLyp;$q6&!T7lQZEH% zDui6Wm^=AE=8VQz1I^cmva+%kfm>8RK73GhU$Atk>g#>0Zf|$A-E!CANK>ewcX^pp zyCI+A(qxVU_KZ1C)+sD}DRqZ+s^byW76+{ZwVFX@L2;p;XF9YVXQkw5+upjQ)fKA>3HhJexNnT!FhM;$=cm0)W<_?!rnl`^?YV}p~;#f=F$ue=1Ej|eb zZRY=U+S93-DQs!Yg^Si3RDw49?`fSft1{3FP}Pn!_>J` z4b=HpFI(0oub|i>@W>%HHr8y*-mDzcsBz}b@t=h+rC#EFzZYf}M5;A6E{IVe8$gL^+-Y5T5;1vC*x!*~nk(v9Y z@C|8(Ym75a_%}|G(tO%6WA&pgr~3S_vS@v;3lLg;=bA0A@dvYM4=R63y?Xsxap{pg z-OC?$#M_+VpZdhG{F=!-7AG!?SF?WJ;aohYQrzF&+nZZTN-Ah)_-|8%LdWMPzD6=A z{Z*KATu5mvgOHHY)kmK$uZ*pqduJoBqTocARXzm*4%G)gnKRg^v^==-StUdxA(TT^ zXu~9ro$JMzXYec#>nT#-=(sq=z?i|TEXs0=<+oDtU1gK4lMJUVlCHg(!`~IbvLM5? z;G-V{gYO+ zADEl)Q=!x8`UAZcJYE8i=4mR&@Fv&=Wz}XG9NaH5QDVW=@}+@lD`c*{yK(*Q($E?0 zq4DwkZEbARgu{OLS{~JQs<;zkB>7S6bNk#?hF3EzYgN8$z0Z=0;!J$^c)`<_y0=G- z1%!kYOBuGY2F%}_1In21#GH7F#3yc8$h_KV#i~_$SH1IDHV8PF2W8cI9x-w1nxMgz zC@(1*Rgze_cKOd5wKRsHM|Ef1&3{GsMLaZrxGp~T-^7LqI(f-`J2x-8*c%6FeDXi7t z_tes*QWkvPn$tZs?~6ITnfUYE*KgkztzFwI&!Z`+T(7!yM)}eWaTlH&8!`)dbVh6v z|N8Z-gp|}H_9(@G9ZD{rW9HlG&1qS4=A(<^YMYg7*XkC3DI1sygx4>bBC?+2qxI8j8Fd{^9s6goHnOIsrW1me z%?$??;-@$7(SN^6%F{COUGmo=$E!2BI5inPmlSQ-b@~4_h6zmR(xW8Qvl=_o0C6nxgc3M=xKzabrel{L(a; zH%uZ&R_Hfo^|oo}=h>R1pPR$~?_+@y8$c+sBzq2oc>_ zaoLSYUdYn^(Twt`_I9Q3Hfxt1P}(VS_r=+?2Ah(L4=q0JmwU)=(^ar*<1@<%6BwS? zPv7M9Ea~X{o{CVD#|e3n3;bs$c%I@c{=D(h&(MSES}H0ZY-a`S|MjcN#N3?u?4otq zpJvD&udDgfxAt-L#SaCh+is@K{`c_V!`X~62W-8-X=sAZ$FqLB^nQKOuX)HlpKU|1 z(aa-{tM7NN+q*mHZSk?TqC-!6YILS+>yQeg5RS(nrk*VA4*_5R4_NJ>QU&kpFU?86bkL-T(`e*jg6Ic zzUwa7`c8Y^a*mBPx*7r-UmWWC={R|dm3eQ*Zjw%>nDHS{9&ig4?j`+ z^MCc!3^%N@zx?Ilr<3uCFBLjPxFV(&t(OyK4|}zDiQf7BM{|4*zLQYi-Z=gBDbv*& z=JG2%E--DI^I_j5vsrRCG`Dc`9$gn9FKVeYu`+VOYKx^a@|UWnRpvZ5p3&$U(e?ae z)HnSr&CSg^>N?3U_9?0CD&&lf_?U9<=;grp`2JtNe!WQ#sdspMqNiEwX4okuu_>}4 zE7$E@ANnC8{K6^0l_9Z_D)(A6ePVU$C#V~6PM^QiDdgSv<3B4G8dzPLcDr7}IimjO zt2;;k7)|n6*br*DcymOrJ44UpAhZ2#+Y31OoaU}M9m?RA_r_M<`@O$PvDb5wOja^1cuYVF|Fc{HLc@5{n<>-Y{I zKKy5c`$=9iMU^|rJ(UrLdjeOkJb8N0s{6|YOM|7{^46{Gir`wOsi5k$lQZ0rMYs3Q z>dHrPb(UT(5zjmGPik|yXudz5_h`y=gQdzzqT3i9goKz^|1y4ly3S!k%>mq9I%O@2V&9p3jwws}hsUiDms6_X=N76;i*QfUFSb8PAaCht5*w5;p)wc1l z$(_$BpT6j_){z=*=7p#7Z@#TCi1yo&dRk0nvgb76X_6)Jf zq^@4Qnvs!l;^dxNYSrwM!-byyi23n7H23q}eYbzz`x_j;^U>Sq|8>Ol<<1FisHp8& zuffV7e#-md*O#km?%nJETmNcHWo|u3b42O0>Y2;R!~HCi%^4;!iFFEDZP02>d2>$t z^O7l#8J5IEd{VfwSZ~H#2Z|gX?~6D3ZFpf#bKUD* zHvC6BLpS88r6m|ndldOIYulWHgoFn9I?4KUPQ{CH@*bHh*R6944i5fSywZ9>p~~k| z=VPCSie((`eEMof)9;?I*&`0DxF z?>GMUXE>ER^Wu^d+`0atTzoYfG(W#izZ3q4Nr)>#Wr<_tsswv8^_kZ1c0As5+0Xhd z2ZMF2cK*S8H+Ua!@QvKP&gWM6|I^xDuYD@M9k`%(OVswqMUmB?T2)W0HnBNPy}oJg zQ$7!+PS*-~+ZWEEp`xEbyA_ViYuB@~i8hO{-NsfLEESa6?6qj>!T?s0DQvH%2sqV$ zb5wU+o$&k2{;s$wR^fGr!zVOl@7Vb(XHr#D*oOH5dZr~p8{O*eEAk}%Ee}t%YvvF4 zv;51{@U*#wNsRNYTkNq9x+g0cg7kELn(?_ZmEM%(Jbk`YH0^g+^Zf9*`s34=wV4S> zW-kiyp@k;%Kv56o?0rfa)GImn`L)Iz=;!+ zCP+lgoLjje-s?iGt46}a3Hn@yB69C`y*No8HT-v9Eba(Z6L}Ud z=;T$dW5m9?A>|rCD_D0byOYYXJ4tf$Rswe$(X3{bnHlY)3 zyMm8sZ7(+ef56k@%5jkQhk2SrxFJ1{d)J^s^cUTorJfX`E~4gX{uU= z4&(Y80g2CMV?>w|G!Xf-l)C5CSc9Kgi|k{r`xGNix=dIP}$S%b>@GU{?a2` zJ=%Ak&P_1CpY>(;s+^7xtC`8?ZZpK){yM+%^!HdB<$vt$Jo{Y?-t5ty8gyu;1sg-b z{yFE1K(pZ+1=nX~XK#KK`t^pC%DERMWpPW}%{Aw$bY!GvZ*%e$%x8>x>@eTsHpd&b}dPixj@(JI!x{D~Z|^`k>If`WD^sV?r? zy}-!hSj2%zL3T_F;=MYrKfNt+VA_-f)yuA}H>C^Hmj?yLN8Sk6`YIdA)}K3b`{{<) z&%1n-Z)@e0SL*ifJ9k?*?tH!U(f;_mKN~lRtzUA!INs3Pi}jJvnjPojSeOC+65^{j@t58*bIga%A%4C*sA&6lTuxF%vpSMa5U zO9HC8{DLlahU$d;SL*EO?dNrA^Eywu_tC7*|CeO~3UqEK+- zn$9r3^RccvKR9Bz4vCt`TXRLF8t%I!+$dYl4poJZhb?EJ@$yo?xtx$yW1a# zZrST0a!Fui6Gv3lqiKh~naCZlYnl4JF7VB%Np*`Re>uN9s3=KOuUP1U&2KUN`JKrH zI(*K%eF8vJl|e!uPZ@5w>$)K*O020vwe)t)@0~lh?~xPdj`}DUHIvi+AxCJS<%UBA zsiNOq9UQ%PafP3Iw_5jf@!jyGx;^_VJmPz46RlncK5az8}xOR`^ z>x+wD^mQ#T3Ai4bdbCt@+5*K<7lbq zx~9Sa(y6ZS3G1l{mASD_F~U_LY3DS~s;xVxNjdF(WAJ`s^Nvul#oD2PT4#a|23c)5 zq@(;re9NArTICP5J{NO1M@&DrkLk!O{_obGo8CNo^keShHD^UOoOrQFrtjq)oi(hV zWS{Hk94@kdy|j_hMMKk+b@K%mtrydorEL1XGo1FVxTGT(qQ@R`b{p5T4MDMS5q=S| zvAwTeWpzc!^avhG?r+hH6J_1@{7#;Dx96pG9jd&px+^1`ILyCK?JIct)#BZwsePYM z`mEsEY-4B@9v0Cw?OgKmwW=FaBkP~l+|XB)>;7Lkn@@);B4tYNu}K@(bMJe<@Asz2 zi0hB01#I5XG>y+Gsx0^Ao@whBq$+>cdXT(eM~+^b%%*3XyHuO+{S1F~@C~ceB%W6r zCY_5+)>C|!_q64>=oDe_q;pdAo>LaJj0>uKmgQW~+;G=3$EiSY^#lEESEng$E0)TC z;qyK;^~mE;{v-cBZ+ltO6}3WZd(0YxI=)2hYfn?+*WUcUhP#M=-j1j7Pi3aGFo{W; z{+c@FsZfr2yw}X4MAkEhxMW_)E!5Vwx*lr6w`Nb!p&R_0VvfkVPM=z6P<>&Kw)sNu zT_O@Ya^(;BRQd#3DuT;Zv!ko`Y&vLcHKBkzXS!>6hP9H|9M?5A=3Q+oo=%^bY7@Qi z{o^8m_4OcKYM&f#r5iAOLpI!{f#A{^Hmo6 z9q^jg+EDuc@Avu5C1Tg|w`|&wB=AD6DszGD^{LqkyE}KEdbsh7^oFBB7ru&hPDv9D z;ZB?0b=v9l?o)kB_THY8rVbwVICRtQRj8*M z0ZME_8&X0g_g?>b;l!ay6Q}c?66y@u@O)QM^_oXpf;e;an?D__Q9KnrW6~}^o$a*` zHtKBIqpj~A<`}(gj=dM@3 zHDjS!=_x{>QAV zpLojsOwmi>>wC1PZkuVrcKp=g3kMTGQ+{Gcv=yH2$+A>_x~E8JS&55Ic_UYs(zKgB zEt>**Cb2b54_c{sCtY4=`hH$1iwURH#l4T6XiVMSQ?TPe?bdG%oAv9SWsSIxj zyf-yHx@z|PgFSnEbh;ug8kAq#GN->W+11IbD)NLSTXDZ{l3JYVxf7bwR|J;1wJ29B zP5ai+6`g_q|@-6}_`x_D;AzgU7Qz zx5_eQkqsVGxWZ~J_ugffulXRuuzJs?4NX&iA9Y>Ab-pz9dgIavbMqyK<@b;4aBIg4XrDQyZraaZX#FAPlx7jhDva!5=5N_q55j<6u-3!lWc z7i~A}SsPLRa@Cn1bys)w&;R@N5%UxQt_YP))g_VfZSQ{urfl9ICYqW!bxTQr`v)$a zjF~&uEt&Y5YhJ%7i=KHPcZ0ykIqnLZCOOJFo=&lR@b1K^oxv9hQ+rdAR5#6xFf}#Z z_*Vy18Gl%|Xcbp!X=&v1(6nui*Eczu3vx^G>FSnGTh_63R&}U`!@=b(7cSR6y>Q;M zrCajnVKw0)>yvsaG@XY5e}rzWTka>hZ$(-}m2f?!M8m z;eZnRqy#460~`j-lRN^Jq;x7w%@JlOW=j=Zq7%Js$@e6f9)IWk z+JB2ahrAZAdgd7t^5m0r|Ict0z9m0}Ou8GU=6m0=ykGzSuXxQv?syBy1N-a`uP>M| zqxa;Cby2YbWvlOT{{9=9yYyI4rgr6>3%@FRp1&^NnLLw)fBL_ZKN@auSR_tk-!+ZL ziaYRLcV1Rt*wgy8f?s+g^WF<4y!>&WEjbiFw94C`(T{NA$V`%p*vT<# znd5ct|9bOg&2TI8(~f$!J#;FsSP37a!{_a>3{K1@^0&DjPhD3QRXY1o$odP%ZXHkC zA6tL;@v436e!D#Ea4OD8xBEZCf2G2L(0Q&OmtQu^&R(AT?(xj(P`Q(N-B~eTb3d)0 zSu6j3+qF7niDTQ|=W71_Yj>~iNoDwg06UEdYb0$mnO+>olH*)$yHGvv#Gxwj<|Kmv z%LN~{1}#|O%(1)UO6k+d7N-A8 z5m-M1+t=A^snmF_F zBPpwQDoKhwg5DL|C!ai7RHswZ6_xk2)yZpz*Re|$52syZjXh|f#kgkQ!gobY(_*3% zB%ELTe64$wf6Kp*eT&cSzvH(f>8C;`w~k_t*k7&r@>6dVi$5}C2yPL0oBtrT=WxcB zjy30n_IKQ7>U-L-@6^ATzO_+VFK%9cDRbvoP^S2Q^QbRR8&555X$ZeJ)xPl5Rp-6)$}i`?+*tVMf_vq{%ri+V6ZbsTwtt#` zPPOpgHr1Uq8@8#oHXL}c>))l{TjgJ#nI8}}Wdfg$Q1FWbSz?Ud9X>(l|1NBovx?`~ zD9KQ#m3Pc+^=`G(z0m1zOVxeUr%idyHR-m>Y4)q(8YP9%H+QYNb2mI}dYI6%(10Rt zR{gckyr+3LsMtx`WAtPW%YYLulO7uum{wg^;xI(bIJd;^5RZk7!SNY zWXJ7o`Cn^I(E+Jv(VYiYb5%W!=9FJFt$l*%Z=pjS-fXhB54m^jF6>&Yms4r==*|)S z(-zf{r#MSvmxJcSe3jys25E9AE+}7qu`0-V6Z^Is-GPeT5${sl{BGNXDSy!rZ_yV# z?YN?;JZ;+I15dyFI+o)nn&vrY%?Huiy09%vyAB@FtXFNVd}8<^ul}R{@~_Xoz6o~y zsOA5#>xkqu0kiHpRhLDqYue@Oe*9_W7N4ila^pSQAFg5p>0p=bPL^{!R^-Nn3x&id z25i&0pweium3hxxJHv;?xBtuV_x>(D>&=$Ce4(6bb6;?HC#X1T2-&$jan9xq5(geR zSt`mM<@++t+cP5Q#*7c|Jr}h+D*w~EI_PA=$#5uE_ zpAP<*E>iV%Wn5(e$8DaQ;k)}ZcduVHCH`6bL<>juu*(_|KWmsf3i_E}2s=IC-N?s$ zL-YiqeikXQPx3gYe3A(Qq@0BU*Ua{8ZhhP0hFQ?byE2k|F zER5gSG9loA(nf*$w#olwKucJI#lrd(xlhl(ArdfyYf9!*{o`I=%l|V?x0FAf|1)7u zY{aokZa+Ucm`>#Cy8ZWAtmPRc=aai92_E!Px@Kd@p6r?)c>+9Jd-C{>O9`tYj3Nr+ zN>_G<7%5HrAha>Xo89(^MtGvPt-818k)WuB0@G@*7SH(684-7gN5F4pOz!qd@3_Dz zBERa_-Z>_7OrUDt?wubZF5BO;{m+#BO%$|Rb*{eJ&p!dp9EDGH)|emJEuoQ?*gCUb zbn9g;o$^qXb=vpezWJ{Djw_>9F3)L#*Se|e_h_5Di)BqOSJiZyrz-aNeSxLlai_`B z-AZA}2R=C-`X2GQ@c&ir(@UFM_pBDNn%3|9z5cAj^P`Xdc7_Q3zU!*v(H-IBbfidp z#UG3HDn;uRq~G|D>VHQ-|u``-51#iH8+L{r_SRo2T@^6ZO^th{ivHuRU=f(4+V zuquuS<>uZGHgffUK7MA*5p#OL_J_;ALp;Z2O+h$6&+W|Cj(YwlySSuo<%y;U2|VIk z{`{fJk)K+UB_|FF{qtgrJ*w%Mcq;$og7Dj_5k7*akNYYqw#>h}!}fZJR%?k>>^bwC z*b8}Dl2e3Mx&#P%dwkuPtG_#I;e?3VfT%5-ID#AM15Yo_{o}f3K~v}t$M;`FmI{0_ z+utR*3!YAkoei7Q1q#t)OuX{tv1@D+iXLc8f&QcOt?={C&Q2WGp>39~WA2WMe zD?)|V1qpMfHqZRz)m)@BC2`e*iugCWNsfU>CJC%j*y$T|r0DEPp-q+%e5x&C>0v@o zRu`~nODby3Q*Hg%Vzf24j&-fm%Y?u^m$v-dbMMm{*F8!9L_2L`S~o z-{#|ypU36vZ5V2tIWGQFyS9)!`GS&BX^d?^qN&65#}?~ZB4&%S-MwY`u2?!aina4_ z$?1nXcX37e<;rf`-4&%*t8#Ru!RK`<@884}Z4_e`h*>y6z)46YX6mPPJ&!_DmpY#P z_-PSi!&$E8WoCTZBEN1v&)?mjYo@Ijwl?X-tz*wN9^q=ez@>f6a&G#ar>kFG{S*?e z_GPn_{FLj0A@{|$hjKlQH*XZ=*>f|#=62T3nq802H74ut;5yLZxMx$HdcYB-jm>;( z)D-VK@aZsAd^)LqUfijMWeU^X?bhv!Z(A*HvRvDezkKQhj-vkMuTRy@_pwZTTKFb> zde@c|0l7Y%={h>=_h<_`-4ktTs!+XtZHLoBfkk^*cCUL~k?>T>Y7>W|irS&j%%zT# zE3U>0tl#r#-KlD(=W#3Fe%^j}_m_ZUb5&nSzLPUPQ*0d>KGiFF>s0Q=ch!>io&9Ml zbxdHJ=-qIpmAcHw<37w?R58`OBV1&S$D4)Bt33sjm796L&uOU;I$*-^HoxnuM7v>z zSpZ|O+lbob)F}bbo{l+=lwyiJEoa)GMVR!HexeHy>l5d}SGmx4N2w$hVlpPOQ)-0b_ z^~y%^h`STV9}!mZY2|F8H!Q=9`m`j=Lm14bz22d?W8Ir0TCCSaTMh_M&gb1#nm{pZXzUVSv>g+haN+=?BgUK6%F1 z3ASjJQM7nhivL^LNi)AewY?LH2(K%h# zD$2!}fAQ23_7g3;qnU0`-odqSw}&bdcht#z=yo`nq#b#Z#4p zz$2g*%3n!IDVdxj&%b`T>_0#K%%NLdZ~ui!eNoPH{j*#Cx~R?Tr zRjKJG-)=v7MC;#%yXlkTpPEd&Y*81k%d^L6;gQEXx+6YChC7Czn6Vs7Cu~e71OIu72>ScHsU=j*$yJj~z^%Gu2%oF0ysa z<&77f9MSsgo-U-=(xM$VwPkgX$G&@Wo!SHUG|k!VwRPD?NI{>NJJq5r?=Df669RNa0*|616c?U8pQDxX~Z?xkO~?rzMTb@`H34MBG0rW;pEy|)A!_Fvpp=~bbjk>?k3YMGMCn#KRl;aH3ziD zjbApx_eiJxj-`tO=ie?qZ(AHhl{JBlQNE&rc7C-_wK4&VEN@`VEL z_LY5kys`c~=fgMax|Ec)@BHnZTszfWu3104>Gnj~+UqAgr`JleexJIrWWt0aj_hH= zhLc|BXO#Z^`Fy_p`xDCjHhc#duXoShVE^qFxB7M&NY?`XL_sjB9^JzuzL>KJ1mB14luC(vrm1CcS^d!Hi##LHgQjy8t zWErMa86J=+-CM!h-0_t;rqg&vuPC>MgwVd{lhorvcdpxY`9+K!-(B;w2VQjVUShn? zM>k|ngj}xe+_0<7p5M!hE3am?I_=KQdZ)AgbMN|n0eiEO1g=l@2yqLGxf4ENaML(lA;G!iDx;EdRLSGo6}%=LA#)$0V3f;BJHuD&6c_;Nv& z$L^xe#0x@>@sFlV-ViEVrne*}hI5aaTd0x79#-}9pC<`T6Y*x7`p@Oi@&~E=WIw&y z&(dAUCicicCPFpEc0*C%VV%_ZEz&|x9Y2kwj=!J(dwpC7dzhY9z5le9)z=>1_F2ut zRh_C8Bj~+oZBNJ3D5Do@NtK*2o0P;JiM*5hpWR{*dq20%UDuoIsn^>Fi*5A%96yvj zk28jU()xshOM3!VExF)(=abl+L!!$M32n(zZhJb>{Xe($#p1Wq4(*Bd&%Y)Xn^+nz zc~pBE-z1kq0^ik=?%w@>(Ih^|sbe{V5Br5YzLIhd=k0&;x>VMedhCw2URulObo;MO zTW>(>baE#M^$~r3!4-mYgD)&*{g?aHqF@ zW9*k%Y8ON{9ony`xa8h@*Jp<-xx>>wuJZmF9kn@plFh!GPs3zG#5TsXY&oDUvgnS3 z*O8)_g#!1QrcQE5wbXwu$|-DEYSbONA#!Fw=2WN1Lhsbl!v(CmTWv4qoOVtq$W`1C zdpX#0qHJyWtIy$atm)rFf9iUE>{8*KCNQZ@Y$HRCkkf;zLSehZ-)2nZn05NN%;|q2 zpH`*Z;`%De`p`s9#*52!E5o;f>-n>s_7v$q_KrB2aOTKrq4Y-~bM(X>8pt#h9-VaP z(pkOJT%3w$RpOSaExTA1voYnvBE6M8TF>*tHNH-dExll~#(SkgNVrDG?e|;GZ@zM2 zS?vmK5ht@Vw)pKh~#%wpkN+AOl( zLues$w~5KpASXqa-?uCH9~+A4D!o2zs``9cQHjq*@I?O|u2*{x3(e_LwGiF@sK0_WKT&vFg0)tk&aRD3hp+ct z`MlR%@s{(BSANQyoW zUb%4Y?o~eypDqo(_w3=OO}E2dPG<>UQ~NUEuNT+hvIIqy^w1ycbQ^8h;@7ZjSGLW{ z4csH;cC<28bOVQ{PD#SVwr5wv<88w^HVQJ-btaH#i{q5>|uJFMAuuLmu`PIJAKjTMS7>V zuXj!S_?4HhRDD{?{0(6mYi8_x^Zq#V+5ioiFeMQ#ZILMEo4I|O!gur%%8ZXzaIvaS zpK7pnnMBFdLm$F)*6elmKDKqK*!_9#a(~ZQ5+Z5TMm5PAsKztjKA^JLYE7xV*(tnOp4DnI3vpH`a_#%S4ZSLrtmvz@p*@) zdF#KQ&y0Rkoa#=-AvA50*5X)cHvf9Ioi*f2y_D~j0Mcr9f zgDw=wmsTYBr(SjRJ+GKt6u+@#f{BWbq6+W%?Dc!g-W_C@=V6G~o+7lb|D!~^*u;lj zO`G}^Bjs)cB);HmE6zM6fTj7_h8=_-gIuVL3)$vj@X4u zE^OPg!6I+MB&Vs94n(bqknk(8?rLpjYSx!*x*dJ&&eft<+t<64zD$_+NE8&bhbsPl zy?);I#e!xr28$0>g~BW>eTM~~+346;hAFPq(AaxRVb7+uN&?HAL^e-a99y}2U5t96 z(7M$gVUjKW*QYjlh=hMU<#}S4H2-WDoyWXNt!uJ&Fe!FEuHGbhD(7vB_JSR|9#qE8 zJ2Z89$I=-~LsXMIzx9V4*%av-5O$=g_dtqJ|DlM57gp}|7d)LA?X%qX(xIt$wOXAr z8rFw$P2QTM=oC}B;s46m8)a{&9V%Zk?ca*8SMqmH;dxo1rNWKlBKv|3 zS_^MHi&Dz?WBW>!HSzX&|81gI& zMYAGLSTT1OaC}RTR8&;C_Gqf%<5em#Tjg%A(}|08m6o1*@V~?%cguww8|PeD92*m+ zA+jm8%}+AI>~`GCDlNt0+}CrCLHU0W#o@AeDDXP-@q3Fpd_Z`xU6 z!2c|6B3lce&LxvQKcCG${rt>KX2l~n8E)M__I}<##x!hvyI6S(8 z;&LawJ5t1-#qQ0Pdqmr1LV4?jW2U<|#vPk|%ZfLnNW=@<@-&&7wjV;>FB}Ms`R$Eq`=2?4u zistN|`+rW}tW7OT`&p3bIYs2y=|<7D&Zl?1T5!$i+q!G|bO@xaag9hbQk&aTQK= zypk~0k>%uWufB>@(M@$Ns|(x?g=wp_9EorEbou`0XX(7pnxAdl zuBD0|(`xnG-@Y|z;s1v6hlg{Y-b=42SKSf&Vy5rneLv=|2Dg1$Obnbl(iy)n9@zbe znWw`sx>1W~qnGQPPdTqc{WLyCY_+-YovWGew$P8fiCU68Y7LRMJiEicp4=_O5mfe~ z##<%l{iH>Lo48V2e_vg@=0ZhJ+KF$9OAbC0Xte*o?P(MDEv~0c;$^I2bDp@ac@eG= zBD$}|C3^i;_S9pA!aJ9IJJ}Us_IyE1+6)f)-M%XoUd0A+u;}Z55j(nI%c=$Qr*ibJ z{rvPnSw@xL=`DLXieD};{M|L7twptE$;*o0Z?{i>zyJTg;|w#3ZMhoX9q&4PF-T99 z$G#(0b;;eW>-&#SoGPG|;~lqPcgx~apXWJ+rF;rBT46Ikcitq&nUD1jYDP2#7ihmb z?p?tw|4{h$CZ~p_B`p&kNbC<@b11ZGzK)yj8;9quvG0y1buFzgyI(i!?Jx5yxlKn^|qeMwpi-+AikpK)IB zzc9Gq_p)!7!{agq*Rz0I!yK6&^H>dnIWE@O<{jEMqxEIdrYBl0|2D66igh#)j0{(5 zJhu8nO!&jDz|y$X4UR{&mD3-I%;^l?Aocz6x<^^9*S^~uZ{kpNY}MX<)zjmw#NHepIr;+Aeu z36T695w2#!6)%@GW&Tv2r$=-{KWfidV7jH4ziQph7?Jl|>dxBknEOaHGa|ytW7eed zdzI-s1s;h>cyk&?2qw9h+zi{0&Asy~yR1C3-6`hQ{X$NyxkWSmKhCbb8nS27x(6$Y zPbM&Tg&)(6KdNcL++DEY27h}(ea~Tu|&Ww_iDf2efs;|?);@w0yt#4 zA1rM#jdi?mxinRI*^jK|2kW#CA3L;v&C&O}w3s@B40R;G*nVH@crwBF*;A)q`8CTl zc5GxwFnxEt`{<=ZZ*OGoQe=H`OsT~pdH1G-@{J({f$eKEQoq|ieR-tm+3h)xE$W=t z1Z2ElqP6{_mgSetPKtA4qx_b(m95i`ZVx+oR@8ZmR{PAohgX^8^SwEYBaFZbjO_fisLQE2Ln*k1WNo&n>tUzW!4B5;(6Y z{IvdpXUl}V)@lDNJS@8YknUUWc@?RtrtgkN?dd!FV~N*^Ps%?c@{c|KVY#5e^^pdj z>dRRRGo`+~Gco#|Vwe?ZZKW?G|Ij6t+h+FlH}hRK7^?iv=BiD(eQL{PpZytnzhi%8 zXZ?NiYmep2O4mk>y7|qE%jV})oSOBv_^QtO&CfQh-!=EI-FLgT`w!ojf1RJWZLN)v zz>E4F5_i@u|F!kn#`3ysSsC*N4@v{7`O%K76jjzx7} zXI$WQ=F{sFLQTa!PLn(i{!ElVGwchlF}AJQykF<+(8SMs?_c-PIFz%d=0jZ=bp=T0357 z{W{HcRx8rYdjFpKzK)?7v_ja4qv1Kj8Lg>8=Z^WDF_X3`@P?%z2E!&p2Nl|1+sQ7=DSvTa|R|)5A|@GTHhWjp)@r^px};4=vYSVNshcc8^!I zUohZ^)}PgLmrHwJJ@{1g-M+eG+9HdxzRu~cU*GkbQ|_I%{Gq9LXK0|)F1tuIdf;7pKtoLSvlOgZ<1F`)%ZWZK`PZ_)3Z`1PPyELtv-7pe>G=_p ztKLkT{B+lO_jzGgZ%q8hb1lq4;k$$4cP@r4KCf>--v6=x_`(PKq?{c0NHKb|dro#@ z7gJ1|*Y2*Fct!DT-I29XJJ!8XFJ84yJ1#!kw3AP(X70@WSHky4f1Q8z#d%r&k6a(( zwqB?b_T$KWqMysIs&?ezy zPtIuVbABrmx@!OFwH_gZrM2Hg7WTFdQThwg$s*()0(1Bn|={<5->g{|+jg^qtunZx>kck}%!n9Uj)~UtFhj zjlFvB#1+|6D-YgZ_TzBij7@fYb0(`wv&j1%sGgGezVgY&=T~DM+I$VMk2-e8^52{N z`&mTRb7(pOds6%?lz^opL&W*UUwaNzf5Jthtg>~?u75Rf6#U!UU>10QtNleKUeVPC&=bM;>euv z-sbc7>D?WyE=nn!4AS2!jRWuhT%u^tBl?iDAq}*9lF#|dr|`TN<_}_hdnT18Snl4~ zX3oFx`7GU~^Zr&PUOgyygMItuT1Z~Ju_5trTdHx|8HTQdle- zc-Nz?(^`s!MIwGU<+iS0|3IXMb@>lFJw+A4`xylxt88?FMHYEXy8rd6YO~-pkyBjZ ztIn;tP`7Hj6cbbP(MygQRr~K*J?ne7{{M@l5HEILZN8vAN@sakb(~~w>?nM^Nzyn? zMzh7@ht(Yx#s?8+oWoLlr|0S`dflA*bi40?+>5){y*Q!tb^ps%?H@w9w~H&P$mPuN zRqQf(T^hNlCBSio(RAKb&r^ODrHMXYohCl@m47JvG>89tcW+qxV&A39GkJyg|5MI> znc#V3QHAR)Ij)G79sV5g3>(B7zP)Z)I7Ptj6mz!BCAlk|qJPVrCKXQ>clpt^`?c1J z749Co_g5E+WSO0*Tm9~SV9S+{hq8j5A}ZIs*;k|~T_UnoiMOKvRAcB5%PoccRbhF0 zb9OKL-W}qddDSubohU1MgNw)k*-WLI>NXIOKbji>A2D}nOK5h=Aj ztBxRSZr(mzx;n(M|7UIe4vwn}-~K65Si-@o<7D&W#p3>Hj6WC{)(NlvpuVTW@y$w~fYoYimhsMW z3f=lcS%pRa23Jgh;EXrhTwfns^ttN;o0+Hi%H@AvEB{$6xX)brhq0eW%6wLfju52> z$((`SmGf60NL4OVymQ~%{_{_pU)R4ZN`LyS>9_a!8jGjr|36Hc(R_PmwQC0}S47JR z-=KJnog3C2_pR{S)4^z%k+VQAe*%&=v@x>=IavBj)6KZ%CeHpHq?XD_VrgHD3 z>ZPgH?`M3Q{=bAV!px&XL}UX;V8ZEXx~J9r=hd(@IIlab#la!dU3sEw%}=Mi*_Nqy zRW@oR_PS~@J`@ms`D{&7vHbB`(6tAWTwyILJ*!07b)AG(g{!U++Z7Y0xur5k%HQ)! zk-)lDzdY9@MAVqgdgO1YGGTYQ__o;1tPBg5UXu5*e?DK&#dVJnXGF_{ER6)gMd!L1 zzOh}Xn;XIEkif{NZ?W-DS5Vo~+D*5Gp6_~OI&;E~16Lh9Wj=sbf?SsPS+e8Lyi^Y> z#g7NHJZ5f}Y}H+@A-d@Isb3L6S2o>E7g)V&ZsgLke|I91JQmdVI_|j?{MREjp3V6- zXW;DJpHvlG=6X(6yUUWm*-*yt!~BnKT9@Xu)m1b2RzFahdbwr)wsory%~<~YT(PaX zlSi)_qZnhiw&0{F1?7mP=_#lB-RIuxl>PrZ`21^0#Uckblus{Pk4RsKGc`oG9-)!lHB^&Ewb zd^%1wA5N;z*O_aQDa4>7=(J^jfA5TtbB8}g>uK!wb$!y+{krREON&Zy#DW#BhwA(- z1BKSP&Ar!o?rH3@+PSB9Zz%j0UE|2vDau+O_|sfowRM$h%cgg$AFb zq1_X{6`bO*J*I!&B;?!8eouBWMZ=ja8q)-V&L|n3=nA{)xBB9X7`t;DYdsG<+S)v! zBF38UZRCT~m7$dp(&p2)2W!vBUcIXSqgb`Wg8LJym!?O>&o4db|D0K(P4#=%BT>bB z7aCnX4C0tJ@OM>4R3}@dC9QhQ61}cvLuE|(86!m%Ihl@HANi~P8%1wl)K>KgxS|yI zUT}dz*ixP(QHCvZ4W;IuKK9X7S;aBp|3l}qm8?@_qXk*^xyyfeC-5q-pg=PDHY2aI z{5!|kkBqE3PD~$OE}yUW_T4+SuF8m|K~Aq%wH#!+85+hD zBEI5^@rF}-r^JM}O!)EX|3m9#$IBll{^8UY+xJ}Nh>rQerbS|mU5l=MY09|zZ|TJs z3nE-SoR#FCy<5J-K#lSJrl)OflUygBPChT)3aX>*joz;QZuS1ZMDcowTYeo%I*KYP zEW38^j%O@bA;6@)frs1Ryd}T(q6yzpcC6F-eO|)i{!>dKC#Cre68}X1iv78=acI;9l^4(i{wrRJgyN7p& zU(06rFFAFB%fnDRjooYC6ln3hy5Y99#h^>b+0uO4&bb<$;o9Agrp|liuJxNsBTi80 zPj!brqvAt@#6vB!19VhHoF3f&U_PTY^Qm{aisDDZ_(6B5Q$@@T{7e}a?w?8072^u1dx zkhYkoNPWq_>%0pd99Z>>BU;wK?DmpFw^MJmJl?m-ZoR>s2M~{u~>G!OD`8R4? z?$+Shj8NC-MBn0kJI>KI(ASfghy*)41s_sw40*$2F2HzPyoL4I&yp%%c#D?Ef ze4Cg1F7NO8O&zY{Vu~l9m*^aMsnH?fW_Y@*Ye%YRoJPjM>#xFMscx;~Ch?y{xL4f{Vi z?#?abuK51ub%5K9^G_%4E*G!rTH0KtwYlzo_d09&n8FDYE-Ywp_4u(zr)5TOlAM^r z28Pc$b0SPRGTF1<>@r%jM&shMb<^S>csGmBQ1bw}FK1uJaLf(A;-bh?N z9-@8r_L@VJ-q2dFh=_`|&QN@X+|fTFtH7T5Es)6MXXSZtuLQv%8lzNA155 zeC&?)uJs(8j9d{dEk6GK=P#{Wr?()&OXK~KPl_3l>OYU!8kWXJ-i~n9aq=ks>^i4K z|Iw09kE=gQ$Qg4)<>eeT%i&h6);?E~sH5iKrx4YVT(GJoA@!-DO|;nhkUQb0mNsv? zFxA!LuEDPLe80YA1iwsp`FN$M+Qx}fL}qPYxnr}|q3Jc9i61vE`Mt`dT->s8PU2BV z#lyz|2bL;n$%}eiwuT)stY{c=odd785r>y$91x3M(tDZJD?EdVfrtN)k?$ek1 zK5aL8_<_CoMdZ$%AEaKatkb<6CbWLXBLzi8XU6UcEAp7Oa(7*#?n5 z20J!zg?kB3a+s=eed_C2&U`-(MZ3rz;iX2_8m6VcwEG;g_zrDRZohd@U<$(VYEt}ry1|7CXxz{)^*xfCCI`Yb=f~=o6)i=w3 z5ZL~Nb)9weKj!kZ|A&j2Tsb)Vqdx@i<6v>{+_UR?_{#*}JK?TrYpf^K>o_SLvHCvi z^U1&&pHBv|svb8G)b$tCExyq8_})TYom|^noFcJ5r{!+T?Q$1idiR(aSO44N#x{D} zME_QPxqrKR-qhLLzrTLj^;duILmQEz^`H`uQDg&0r-b5>(+s~D9$2k8bh$#1Iq5=W ztK;preA+_0o+bah8Gei3@_Oh4v#W34xae?2wg?F6w%%ENoN>i;zOp!@dGEBHg!=B* zWS>)amrToj+POB>Ed0+(rHGrIfqG0+@0#7NW18nQ{Zg{{MD6+7UE$&4N40%lH&3hL zO?xids_o>|#w4aFxHk%*vd$fMzt!cjX`S`~*EyTEgqFu~Ua#9w?BChY&-?i4@6Rvd zH*?!+Zx@O8*(?8Y+im_mkM2)zUfSH^!ezXHV*`1t*%;#tR~B^w^>n^ks2^U|Zx ztR$(GD`)Q7H~puqzsHXIAKB|_mSoNfJF)bV&*}!TZ6dBZyA{fvt8a#KozAu0c9MP1 z>9kZ&JF_11KXs~GWM|!5pMERs{LV+mo-U|x^>`9+Kxv8Fn>#x%t0dKG*d0nK32)ye zz5dNL+pj|Fn6?LIr{@&8KfCW+{Xgf$qFFvqDuP`&GE254s{d=KE{qS{^UCT?@riBy z6JMRa| z7QU_8R{M31cAky+v`vm(;Vmv-?w*+5FyWR_+L;;CTLg+36IiDuGCo_#8u_1Ts(;}s z9rdFpN>j65FW8ABEf5KPyxgD$wEs77^qom^W;(*eUZ%oHmGkkO1I7L8UhW@k2IS~$b zPqQ+E!Y4~fF(%Y>^Ov2rwTolyPq(e_iwTdCjvPkOr2*F_%$X=%K^P^9U;#cJi8 zU!FTRclCeXX;bt)tZ2PS?EB_DtFOgh4H@QG7G=h&9oti=+_GZR zU4dKcwBlag{d#t*B#CW=jm5d zmHZyA-kET#B$_b{}M8Pnq=v3MbVF5E6!#b|c_J+u}*IlZ$J@&zL+Rn)g1*`uV&s-L&BPg=Z<4^t)7nW!rJx z@26hdo_nueXTNg8b$0gfa|i#OiIJby%C}qh&_4gKY0lO9f~Pmnbrkt5wCj!+*V~rm z2HsqutlA258piLiY=N~?yAhYQ2JE!Ma8$^SMLqmE?ICTw-xbxZ=BX~`t>Kf=e?;ld3-PTW%)I= zY-4tK-Wn_M`)=pP>Nr(#vHjVPzQ+b@f2;bh@bk8e@7H4-4s(P1^$R z>u)`;x#8}m(HQRe3sC&MWxgw1j zrvc>&jL($1*Z7Br&TUy8aQoJl>8~DN@ZYumxWv9=M&+y4R;aF$`|G&-;B}95R*`)# zesmq@RXw`;naYZarRB>f%n~|(M>|@@{{ib-#xap zTBY*q_B0Nw9n0R_-fg{IC^jhL<>R;@j^Ms&+0j#TyBOau+CQQCYVp@+hti!c|7uyb zpRFsr<;r)dqthEE{JMPcVxXQXOM_^V=CkCu6Hi;j5|_oT)3$%RlvP%6?Q5^hHLv)b z)>b;WOmX%q7SmgQ`}wMy`AM2_f(kMpv%V}9PKvgQX!6tfe!gCJ=FfXF^5)V}3+}%2 zHF;|Icbdqp{Ic@ol}|Dt+vSgzV$_znd$HMSa$>$&!dKooI|aU$n)sUQ zA-}Yx$i72+E0Ie;2%=$a$!aF&QB-hZO6$)in7caTs z*${MBFIp?(dVJswttq_k+|Hkx^x5nEWuwJm=l`D5pWggPRPpP6y-%tNDt248Z=XFk zBBJf*jA((4O5B;#<&STPcHJ>4Bmef2$e8ebeS((?v&-@x&TqJ!Ik#8)Rq-^#9g*91 z-vqhEWOha9A+4mNKQ~8mr>(O$*!6P1m}{}_BekrZ5gP?IF7b<=Y+<_IOz5}sUGbdV zYot$CMt`kPQom3?|7PXVRJXPB%D=7xS4cXbM&6kXT@nA4_I2+yJpAq3+;!S)TW|AT zv3N9f#mh|9&>uy+4VP71-E*hw_lN8|oDQ*jm>x;r(rcdE5!!t|ddiuI`h*jc%eo@8 z9=D3W%eyHwd6Lu5NsjCPCQju$R1PY<`g5Jv^Z&e?yA8G$;K5`2-wQVQ`-4jRHiIK` zC(O8b{d&0{C^EL>9})98y5!YV&e->l4j0G%6kDfq>$aKan%7=McMcpckZU;3)H-3FT#>FnM|28@ zzt_5*E~_1UWe(kqjpO@WQ0~k={olHSAA*)Aaa4TgD*v7nE^}9{MYe!H^ZTv$>JRRk z{VfQ4$G2s-yR`YOb8~aE z8D;pUeA>XZw523gahqzhoziwE;VP-vJy}O2lW+9tcZIR6(3720eb7Gn(VU-sZzoIL zTGusO`!v7m#5Ho8vvt=gXC<0nb&_23uxO*qE%p6N1WJpOJLedzwOFxqlfK-)yZ2?9 zPh~YXJrY&?*ubab^yu_!hHvZ_>SV(hm)ht|+`4x!|9M~6>r0e^7JPdSl&P54T$MtuW}>BkcXC^^1hh$!&k&dDiOE~O=hPrdvGbJJo#q6FmvVQ zu4bkem%n_GHs}c7vr9loKB$J1qf_14*?D`a(M(H*H0Bd_x^ag}e2%4*DChoqaj8@^ zgXg}NXY)>o6D^ZfT;^$ge08DqM*79;vfk{oJ@>4#jtXkaeG{W|_I%v_n_;I8Uo#e8 z{X-+5EX;`c-r9588Bz16NpAhskzKcyhwWq6J8h>&%&a<2NoOuxxRCp@WS35oZS!

j#@RSHzeL6r#b4!kAmgmCjb!V%0 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

y4+G9m3+yEWf$ZCklJmwR%= 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
$OsI8?= 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 R@LTMmWf0^Rdsqp?006fYfPw%3 literal 0 HcmV?d00001 diff --git a/addons/gut/fonts/LobsterTwo-Italic.ttf.import b/addons/gut/fonts/LobsterTwo-Italic.ttf.import new file mode 100644 index 0000000..faba19b --- /dev/null +++ b/addons/gut/fonts/LobsterTwo-Italic.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dis65h8wxc3f2" +path="res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata" + +[deps] + +source_file="res://addons/gut/fonts/LobsterTwo-Italic.ttf" +dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=1 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf b/addons/gut/fonts/LobsterTwo-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..556c45e7d1d6e89bcb94c012d14707cd97d4718b GIT binary patch literal 234956 zcmZQzWME(rVq{=oVNh^)adl&fV+mzo);hw#z+mDY;2&K5U~MD=v(^s=hSUk}!J$q; zxp9*im?v~FFt9rL2kRS|?ETKg!1TF-fq@|*%*8jN@Lr`k1Jmal3=FIfl5-Oa^vlxh z8JH#aFfcH)B$t&aFg;^;W?+{4z`($ulva?Q%N9AuoPk;900RSKae7W=TK2bf><-gnDSkJ*Kr1>w^JAx7|b#FpT?2G)+8{NzL?ws~<3OfNVX7+8C96DtasOIQ{%Fg<_4z`&r8 zmzbN{J!#_#24;B?1_st01^LA#vE`SKGBB%v^nWcVN-f~ncDaRtS@TU}IolV9|EuW?*CBWDsNEWKd(^ zWH4jkWN>5PWMGZm#lXX$%3vlaWVwq$j6slrQ&7kftXQ5wj6t43jX|Elj6t5kjX|D) zHFgIBgTM|3s|!0A%mj8YINo8{!N8-xgMq;{Vh4kfzVuE8BL;rENT>hFC_aV0$_QxiKTa}ZBlOjLwTSxF5fU~D8NZpREF1sUZS#Rd788TYFt8u5tm zaqx%iO34c8mo#iN=flcYRekCYH~~PNZNS>=}akW7MEY(W7$g|v8RqR^PymI9{tgBOfgKFI7j`i4>hEM=WnlXP4pmkLF$Pu! zH3n7&GX_=$HwIQ{sP1HtU|{>Qi-CiImq9{M$PyCLk{5O{$iYP<1a>eeg2J2wWCJM7 zMLJSk&+O~Gx9HNmSqpdn*1dG@q2Pf!>z?PJpOO|xWRV&MAU$85*+j=`HDgt2@# zg9ZZ|g8?}42{EuSNP$@#3~UVCV7G7x>|kKN01gWw1~CR91~mpD1~UdB1~&#FaB>u7 zFks*SCr5b(DFz`}a@1fDW6)qwW6)qQW6)r5W6%Hx9Jjy@2LB5TI~YV^L1GIE5?iDo zaR&*wBMESU1h^pS(v*RbK_2WvRt6RZHn0ob1$Kh6%oj&i21y1622BPA2BtzsMFtKA z1_lWR1_liV1_lcT1_loX28IX*28Ij<1_n@x)4{;NFoS`CVFd#N!wv=ph7$}73^y1U z7(luE2Ll6xK&&G(*bv6TT@1_&?hFjzc$SA|2rhx03>*wBUv@BnMEEc4VBpu^!C)$| zgF*WagtoY_gTVsiPl25bP7Ewxb}{%furaU-3RxN$GAgO53mS_m3mS_;@(offqvRV% zPBOM)1ZNyMCSxOTzG8)BAEtZq_HvUa-}$#qEy++yhF46&NK=|iSXM~ZNKKScN}8WX zT!aUljl_8*9lS#Hg>>|Eg?Q!Vr8$}SCFK5v8O10{8R}>-P2}VF_s#m>6E|?)ir5*b z6Qaqj>};tR5{t-L5+c0p{1Sqa>e75n;H<^Y#>~dfETooPf^wpRK5-@BE(R?IB?e|~P+7N=L5zX%3u-Bd zUi$4~aAx3PZ~#XwBtA(j2ZfEkD9ymhAi^NYAi|)@Ai`kDAj06uAi@yI zAi|KzAi_||Ai~heAi^+{L4;u?g9yV;1`&pn3?dA?vAY<=8AKSE!EG9GeQ*O$fkBKx zfkBNyfx(PHfx(SIfq^x4Cxb7zp{vYb!{7_f{JIPp3<|Jjo*9D}gBgPwgBcjQfm+G2 zI~c?Tb}$59*umf{u!A82l*hq64o^@+&J(F+hv;!YyCC8q72=Sl9vcG-13%c`jtq