Repository: utopia-rise/fmod-gdnative
Branch: master
Commit: a3c2dc9a72b0
Files: 560
Total size: 21.7 MB
Directory structure:
gitextract_oki6utj3/
├── .clang-format
├── .gitattributes
├── .github/
│ ├── actions/
│ │ ├── create-android-plugin/
│ │ │ └── action.yaml
│ │ └── create-native-build/
│ │ └── action.yaml
│ └── workflows/
│ ├── check_pr.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── .readthedocs.yml
├── Android.mk
├── LICENSE
├── README.md
├── SConstruct
├── android-plugin/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── gradle/
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── library/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ └── com/
│ │ │ └── utopiarise/
│ │ │ └── godot/
│ │ │ └── fmod/
│ │ │ └── android/
│ │ │ └── plugin/
│ │ │ └── FmodPlugin.kt
│ │ └── resources/
│ │ └── fmod-android-license.txt
│ └── settings.gradle.kts
├── demo/
│ ├── .gitattributes
│ ├── .gitignore
│ ├── .gutconfig.json
│ ├── addons/
│ │ ├── fmod/
│ │ │ ├── .gitignore
│ │ │ ├── FmodAndroidExportPlugin.gd
│ │ │ ├── FmodAndroidExportPlugin.gd.uid
│ │ │ ├── FmodManager.gd
│ │ │ ├── FmodManager.gd.uid
│ │ │ ├── FmodPlugin.gd
│ │ │ ├── FmodPlugin.gd.uid
│ │ │ ├── fmod.gdextension
│ │ │ ├── fmod.gdextension.uid
│ │ │ ├── icons/
│ │ │ │ ├── bank_icon.svg.import
│ │ │ │ ├── bus_icon.svg.import
│ │ │ │ ├── c_parameter_icon.svg.import
│ │ │ │ ├── d_parameter_icon.svg.import
│ │ │ │ ├── event_icon.svg.import
│ │ │ │ ├── fmod_emitter.png.import
│ │ │ │ ├── fmod_icon.svg.import
│ │ │ │ ├── snapshot_icon.svg.import
│ │ │ │ └── vca_icon.svg.import
│ │ │ ├── libs/
│ │ │ │ ├── android/
│ │ │ │ │ └── arm64/
│ │ │ │ │ └── CopyPast_Fmod_Libs_Here.txt
│ │ │ │ ├── iOS/
│ │ │ │ │ └── CopyPast_Fmod_Libs_Here.txt
│ │ │ │ ├── linux/
│ │ │ │ │ └── CopyPast_Fmod_Libs_Here.txt
│ │ │ │ ├── macos/
│ │ │ │ │ ├── CopyPast_Fmod_Libs_Here.txt
│ │ │ │ │ ├── libGodotFmod.macos.editor.framework/
│ │ │ │ │ │ └── Info.plist
│ │ │ │ │ ├── libGodotFmod.macos.template_debug.framework/
│ │ │ │ │ │ └── Info.plist
│ │ │ │ │ └── libGodotFmod.macos.template_release.framework/
│ │ │ │ │ └── Info.plist
│ │ │ │ └── windows/
│ │ │ │ └── CopyPast_Fmod_Libs_Here.txt
│ │ │ ├── plugin.cfg
│ │ │ └── tool/
│ │ │ ├── FmodBankDatabase.gd
│ │ │ ├── FmodBankDatabase.gd.uid
│ │ │ ├── inspectors/
│ │ │ │ ├── FmodBankLoaderPropertyInspectorPlugin.gd
│ │ │ │ ├── FmodBankLoaderPropertyInspectorPlugin.gd.uid
│ │ │ │ ├── FmodEmitterPropertyInspectorPlugin.gd
│ │ │ │ └── FmodEmitterPropertyInspectorPlugin.gd.uid
│ │ │ ├── performances/
│ │ │ │ ├── PerformancesDisplay.gd
│ │ │ │ └── PerformancesDisplay.gd.uid
│ │ │ ├── property_editors/
│ │ │ │ ├── FmodBankPathEditorProperty.gd
│ │ │ │ ├── FmodBankPathEditorProperty.gd.uid
│ │ │ │ ├── FmodBankPathsPropertyEditorUi.tscn
│ │ │ │ ├── FmodEventEditorProperty.gd
│ │ │ │ ├── FmodEventEditorProperty.gd.uid
│ │ │ │ ├── FmodEventEditorProperty.tscn
│ │ │ │ ├── FmodGuidAndPathPropertyEditorUi.gd
│ │ │ │ ├── FmodGuidAndPathPropertyEditorUi.gd.uid
│ │ │ │ ├── FmodGuidAndPathPropertyEditorUi.tscn
│ │ │ │ ├── FmodPathEditorProperty.gd
│ │ │ │ ├── FmodPathEditorProperty.gd.uid
│ │ │ │ └── FmodPathEditorProperty.tscn
│ │ │ └── ui/
│ │ │ ├── EventParametersDisplay.gd
│ │ │ ├── EventParametersDisplay.gd.uid
│ │ │ ├── EventParametersDisplay.tscn
│ │ │ ├── EventParametersWindow.tscn
│ │ │ ├── EventPlayControls.gd
│ │ │ ├── EventPlayControls.gd.uid
│ │ │ ├── FmodBankExplorer.gd
│ │ │ ├── FmodBankExplorer.gd.uid
│ │ │ ├── FmodBankExplorer.tscn
│ │ │ ├── ParameterDisplay.gd
│ │ │ ├── ParameterDisplay.gd.uid
│ │ │ ├── ParameterDisplay.tscn
│ │ │ └── TestFmodBankExplorer.tscn
│ │ └── gut/
│ │ ├── GutScene.gd
│ │ ├── GutScene.gd.uid
│ │ ├── GutScene.tscn
│ │ ├── LICENSE.md
│ │ ├── UserFileViewer.gd
│ │ ├── UserFileViewer.gd.uid
│ │ ├── UserFileViewer.tscn
│ │ ├── autofree.gd
│ │ ├── autofree.gd.uid
│ │ ├── awaiter.gd
│ │ ├── awaiter.gd.uid
│ │ ├── cli/
│ │ │ ├── change_project_warnings.gd
│ │ │ ├── change_project_warnings.gd.uid
│ │ │ ├── gut_cli.gd
│ │ │ ├── gut_cli.gd.uid
│ │ │ ├── optparse.gd
│ │ │ └── optparse.gd.uid
│ │ ├── collected_script.gd
│ │ ├── collected_script.gd.uid
│ │ ├── collected_test.gd
│ │ ├── collected_test.gd.uid
│ │ ├── comparator.gd
│ │ ├── comparator.gd.uid
│ │ ├── compare_result.gd
│ │ ├── compare_result.gd.uid
│ │ ├── diff_formatter.gd
│ │ ├── diff_formatter.gd.uid
│ │ ├── diff_tool.gd
│ │ ├── diff_tool.gd.uid
│ │ ├── double_templates/
│ │ │ ├── function_template.txt
│ │ │ ├── init_template.txt
│ │ │ └── script_template.txt
│ │ ├── double_tools.gd
│ │ ├── double_tools.gd.uid
│ │ ├── doubler.gd
│ │ ├── doubler.gd.uid
│ │ ├── dynamic_gdscript.gd
│ │ ├── dynamic_gdscript.gd.uid
│ │ ├── editor_caret_context_notifier.gd
│ │ ├── editor_caret_context_notifier.gd.uid
│ │ ├── error_tracker.gd
│ │ ├── error_tracker.gd.uid
│ │ ├── fonts/
│ │ │ └── OFL.txt
│ │ ├── gui/
│ │ │ ├── EditorRadioButton.tres
│ │ │ ├── GutBottomPanel.gd
│ │ │ ├── GutBottomPanel.gd.uid
│ │ │ ├── GutBottomPanel.tscn
│ │ │ ├── GutControl.gd
│ │ │ ├── GutControl.gd.uid
│ │ │ ├── GutControl.tscn
│ │ │ ├── GutEditorWindow.gd
│ │ │ ├── GutEditorWindow.gd.uid
│ │ │ ├── GutEditorWindow.tscn
│ │ │ ├── GutLogo.tscn
│ │ │ ├── GutRunner.gd
│ │ │ ├── GutRunner.gd.uid
│ │ │ ├── GutRunner.tscn
│ │ │ ├── GutSceneTheme.tres
│ │ │ ├── MinGui.tscn
│ │ │ ├── NormalGui.tscn
│ │ │ ├── OutputText.gd
│ │ │ ├── OutputText.gd.uid
│ │ │ ├── OutputText.tscn
│ │ │ ├── ResizeHandle.gd
│ │ │ ├── ResizeHandle.gd.uid
│ │ │ ├── ResizeHandle.tscn
│ │ │ ├── ResultsTree.gd
│ │ │ ├── ResultsTree.gd.uid
│ │ │ ├── ResultsTree.tscn
│ │ │ ├── RunAtCursor.gd
│ │ │ ├── RunAtCursor.gd.uid
│ │ │ ├── RunAtCursor.tscn
│ │ │ ├── RunExternally.gd
│ │ │ ├── RunExternally.gd.uid
│ │ │ ├── RunExternally.tscn
│ │ │ ├── RunResults.gd
│ │ │ ├── RunResults.gd.uid
│ │ │ ├── RunResults.tscn
│ │ │ ├── Settings.tscn
│ │ │ ├── ShellOutOptions.gd
│ │ │ ├── ShellOutOptions.gd.uid
│ │ │ ├── ShellOutOptions.tscn
│ │ │ ├── ShortcutButton.gd
│ │ │ ├── ShortcutButton.gd.uid
│ │ │ ├── ShortcutButton.tscn
│ │ │ ├── ShortcutDialog.gd
│ │ │ ├── ShortcutDialog.gd.uid
│ │ │ ├── ShortcutDialog.tscn
│ │ │ ├── about.gd
│ │ │ ├── about.gd.uid
│ │ │ ├── about.tscn
│ │ │ ├── editor_globals.gd
│ │ │ ├── editor_globals.gd.uid
│ │ │ ├── gut_config_gui.gd
│ │ │ ├── gut_config_gui.gd.uid
│ │ │ ├── gut_gui.gd
│ │ │ ├── gut_gui.gd.uid
│ │ │ ├── gut_logo.gd
│ │ │ ├── gut_logo.gd.uid
│ │ │ ├── gut_user_preferences.gd
│ │ │ ├── gut_user_preferences.gd.uid
│ │ │ ├── option_maker.gd
│ │ │ ├── option_maker.gd.uid
│ │ │ ├── panel_controls.gd
│ │ │ ├── panel_controls.gd.uid
│ │ │ ├── run_from_editor.gd
│ │ │ ├── run_from_editor.gd.uid
│ │ │ └── run_from_editor.tscn
│ │ ├── gut.gd
│ │ ├── gut.gd.uid
│ │ ├── gut_cmdln.gd
│ │ ├── gut_cmdln.gd.uid
│ │ ├── gut_config.gd
│ │ ├── gut_config.gd.uid
│ │ ├── gut_fonts.gd
│ │ ├── gut_fonts.gd.uid
│ │ ├── gut_loader.gd
│ │ ├── gut_loader.gd.uid
│ │ ├── gut_loader_the_scene.tscn
│ │ ├── gut_menu.gd
│ │ ├── gut_menu.gd.uid
│ │ ├── gut_plugin.gd
│ │ ├── gut_plugin.gd.uid
│ │ ├── gut_to_move.gd
│ │ ├── gut_to_move.gd.uid
│ │ ├── gut_tracked_error.gd
│ │ ├── gut_tracked_error.gd.uid
│ │ ├── gut_vscode_debugger.gd
│ │ ├── gut_vscode_debugger.gd.uid
│ │ ├── hook_script.gd
│ │ ├── hook_script.gd.uid
│ │ ├── inner_class_registry.gd
│ │ ├── inner_class_registry.gd.uid
│ │ ├── input_factory.gd
│ │ ├── input_factory.gd.uid
│ │ ├── input_sender.gd
│ │ ├── input_sender.gd.uid
│ │ ├── junit_xml_export.gd
│ │ ├── junit_xml_export.gd.uid
│ │ ├── lazy_loader.gd
│ │ ├── lazy_loader.gd.uid
│ │ ├── logger.gd
│ │ ├── logger.gd.uid
│ │ ├── menu_manager.gd.uid
│ │ ├── method_maker.gd
│ │ ├── method_maker.gd.uid
│ │ ├── one_to_many.gd
│ │ ├── one_to_many.gd.uid
│ │ ├── orphan_counter.gd
│ │ ├── orphan_counter.gd.uid
│ │ ├── parameter_factory.gd
│ │ ├── parameter_factory.gd.uid
│ │ ├── parameter_handler.gd
│ │ ├── parameter_handler.gd.uid
│ │ ├── plugin.cfg
│ │ ├── printers.gd
│ │ ├── printers.gd.uid
│ │ ├── result_exporter.gd
│ │ ├── result_exporter.gd.uid
│ │ ├── script_parser.gd
│ │ ├── script_parser.gd.uid
│ │ ├── signal_watcher.gd
│ │ ├── signal_watcher.gd.uid
│ │ ├── source_code_pro.fnt
│ │ ├── spy.gd
│ │ ├── spy.gd.uid
│ │ ├── strutils.gd
│ │ ├── strutils.gd.uid
│ │ ├── stub_params.gd
│ │ ├── stub_params.gd.uid
│ │ ├── stubber.gd
│ │ ├── stubber.gd.uid
│ │ ├── summary.gd
│ │ ├── summary.gd.uid
│ │ ├── test.gd
│ │ ├── test.gd.uid
│ │ ├── test_collector.gd
│ │ ├── test_collector.gd.uid
│ │ ├── thing_counter.gd
│ │ ├── thing_counter.gd.uid
│ │ ├── utils.gd
│ │ ├── utils.gd.uid
│ │ ├── version_conversion.gd
│ │ ├── version_conversion.gd.uid
│ │ ├── version_numbers.gd
│ │ ├── version_numbers.gd.uid
│ │ ├── warnings_manager.gd
│ │ └── warnings_manager.gd.uid
│ ├── appstore.png.import
│ ├── assets/
│ │ ├── Banks/
│ │ │ ├── Dialogue_CN.bank
│ │ │ ├── Dialogue_EN.bank
│ │ │ ├── Dialogue_JP.bank
│ │ │ ├── Master.bank
│ │ │ ├── Master.strings.bank
│ │ │ ├── Music.bank
│ │ │ ├── SFX.bank
│ │ │ └── Vehicles.bank
│ │ ├── Music/
│ │ │ ├── License.txt
│ │ │ ├── jingles_SAX07.ogg
│ │ │ └── jingles_SAX07.ogg.import
│ │ └── Sounds/
│ │ ├── beltHandle1.ogg
│ │ ├── beltHandle1.ogg.import
│ │ ├── beltHandle2.ogg
│ │ ├── beltHandle2.ogg.import
│ │ ├── bookClose.ogg
│ │ ├── bookClose.ogg.import
│ │ ├── bookFlip1.ogg
│ │ ├── bookFlip1.ogg.import
│ │ ├── bookFlip2.ogg
│ │ ├── bookFlip2.ogg.import
│ │ ├── bookFlip3.ogg
│ │ ├── bookFlip3.ogg.import
│ │ ├── bookOpen.ogg
│ │ ├── bookOpen.ogg.import
│ │ ├── bookPlace1.ogg
│ │ ├── bookPlace1.ogg.import
│ │ ├── bookPlace2.ogg
│ │ ├── bookPlace2.ogg.import
│ │ ├── bookPlace3.ogg
│ │ ├── bookPlace3.ogg.import
│ │ ├── chop.ogg
│ │ ├── chop.ogg.import
│ │ ├── cloth1.ogg
│ │ ├── cloth1.ogg.import
│ │ ├── cloth2.ogg
│ │ ├── cloth2.ogg.import
│ │ ├── cloth3.ogg
│ │ ├── cloth3.ogg.import
│ │ ├── cloth4.ogg
│ │ ├── cloth4.ogg.import
│ │ ├── clothBelt.ogg
│ │ ├── clothBelt.ogg.import
│ │ ├── clothBelt2.ogg
│ │ ├── clothBelt2.ogg.import
│ │ ├── creak1.ogg
│ │ ├── creak1.ogg.import
│ │ ├── creak2.ogg
│ │ ├── creak2.ogg.import
│ │ ├── creak3.ogg
│ │ ├── creak3.ogg.import
│ │ ├── doorClose_1.ogg
│ │ ├── doorClose_1.ogg.import
│ │ ├── doorClose_2.ogg
│ │ ├── doorClose_2.ogg.import
│ │ ├── doorClose_3.ogg
│ │ ├── doorClose_3.ogg.import
│ │ ├── doorClose_4.ogg
│ │ ├── doorClose_4.ogg.import
│ │ ├── doorOpen_1.ogg
│ │ ├── doorOpen_1.ogg.import
│ │ ├── doorOpen_2.ogg
│ │ ├── doorOpen_2.ogg.import
│ │ ├── drawKnife1.ogg
│ │ ├── drawKnife1.ogg.import
│ │ ├── drawKnife2.ogg
│ │ ├── drawKnife2.ogg.import
│ │ ├── drawKnife3.ogg
│ │ ├── drawKnife3.ogg.import
│ │ ├── dropLeather.ogg
│ │ ├── dropLeather.ogg.import
│ │ ├── footstep00.ogg
│ │ ├── footstep00.ogg.import
│ │ ├── footstep01.ogg
│ │ ├── footstep01.ogg.import
│ │ ├── footstep02.ogg
│ │ ├── footstep02.ogg.import
│ │ ├── footstep03.ogg
│ │ ├── footstep03.ogg.import
│ │ ├── footstep04.ogg
│ │ ├── footstep04.ogg.import
│ │ ├── footstep05.ogg
│ │ ├── footstep05.ogg.import
│ │ ├── footstep06.ogg
│ │ ├── footstep06.ogg.import
│ │ ├── footstep07.ogg
│ │ ├── footstep07.ogg.import
│ │ ├── footstep08.ogg
│ │ ├── footstep08.ogg.import
│ │ ├── footstep09.ogg
│ │ ├── footstep09.ogg.import
│ │ ├── handleCoins.ogg
│ │ ├── handleCoins.ogg.import
│ │ ├── handleCoins2.ogg
│ │ ├── handleCoins2.ogg.import
│ │ ├── handleSmallLeather.ogg
│ │ ├── handleSmallLeather.ogg.import
│ │ ├── handleSmallLeather2.ogg
│ │ ├── handleSmallLeather2.ogg.import
│ │ ├── knifeSlice.ogg
│ │ ├── knifeSlice.ogg.import
│ │ ├── knifeSlice2.ogg
│ │ ├── knifeSlice2.ogg.import
│ │ ├── licence.txt
│ │ ├── metalClick.ogg
│ │ ├── metalClick.ogg.import
│ │ ├── metalLatch.ogg
│ │ ├── metalLatch.ogg.import
│ │ ├── metalPot1.ogg
│ │ ├── metalPot1.ogg.import
│ │ ├── metalPot2.ogg
│ │ ├── metalPot2.ogg.import
│ │ ├── metalPot3.ogg
│ │ └── metalPot3.ogg.import
│ ├── default_env.tres
│ ├── export_presets.cfg
│ ├── high_level_2D/
│ │ ├── ChangeColor.gd
│ │ ├── ChangeColor.gd.uid
│ │ ├── ChooseLanguageButton.gd
│ │ ├── ChooseLanguageButton.gd.uid
│ │ ├── Emitter.gd
│ │ ├── Emitter.gd.uid
│ │ ├── FmodNodesTest.tscn
│ │ ├── Kinematic.gd
│ │ ├── Kinematic.gd.uid
│ │ ├── SayWelcomeButton.gd
│ │ ├── SayWelcomeButton.gd.uid
│ │ ├── footstep.tscn
│ │ ├── sin_move.gd
│ │ └── sin_move.gd.uid
│ ├── high_level_3D/
│ │ ├── FPSCounter.gd
│ │ ├── FPSCounter.gd.uid
│ │ ├── World.tscn
│ │ ├── environment/
│ │ │ ├── 1x1.png.import
│ │ │ ├── Ball.tscn
│ │ │ ├── Floor.tscn
│ │ │ ├── Wall.tscn
│ │ │ ├── ball_material.tres
│ │ │ ├── box.tscn
│ │ │ ├── sin_move.gd
│ │ │ ├── sin_move.gd.uid
│ │ │ ├── soundcollider.gd
│ │ │ ├── soundcollider.gd.uid
│ │ │ └── wall_material.tres
│ │ ├── player/
│ │ │ ├── Camera.gd
│ │ │ ├── Camera.gd.uid
│ │ │ ├── Player.gd
│ │ │ ├── Player.gd.uid
│ │ │ └── Player.tscn
│ │ ├── rollingball.gd
│ │ ├── rollingball.gd.uid
│ │ ├── selfdestroy.gd
│ │ └── selfdestroy.gd.uid
│ ├── icon.png.import
│ ├── icon.svg.import
│ ├── low_level_2D/
│ │ ├── ChangeColor.gd
│ │ ├── ChangeColor.gd.uid
│ │ ├── Emitter.gd
│ │ ├── Emitter.gd.uid
│ │ ├── EnterAndLeave.gd
│ │ ├── EnterAndLeave.gd.uid
│ │ ├── EnterandLeave2.gd
│ │ ├── EnterandLeave2.gd.uid
│ │ ├── FmodScriptTest.tscn
│ │ ├── FmodTest.gd
│ │ ├── FmodTest.gd.uid
│ │ ├── LangChooseButton.gd
│ │ ├── LangChooseButton.gd.uid
│ │ ├── Listener.gd
│ │ ├── Listener.gd.uid
│ │ ├── Listener2.gd
│ │ ├── Listener2.gd.uid
│ │ ├── WelcomeButton.gd
│ │ └── WelcomeButton.gd.uid
│ ├── project.godot
│ ├── run_tests.sh
│ └── test/
│ ├── integration/
│ │ └── init
│ ├── tests.tscn
│ └── unit/
│ ├── test_bank.gd
│ ├── test_bank.gd.uid
│ ├── test_bus.gd
│ ├── test_bus.gd.uid
│ ├── test_callbacks.gd
│ ├── test_callbacks.gd.uid
│ ├── test_desc_event.gd
│ ├── test_desc_event.gd.uid
│ ├── test_event.gd
│ ├── test_event.gd.uid
│ ├── test_global.gd
│ ├── test_global.gd.uid
│ ├── test_listener.gd
│ ├── test_listener.gd.uid
│ ├── test_sound.gd
│ ├── test_sound.gd.uid
│ ├── test_vca.gd
│ └── test_vca.gd.uid
├── docs/
│ ├── .gitignore
│ ├── build.sh
│ ├── mkdocs.yml
│ ├── requirements.txt
│ ├── run.sh
│ └── src/
│ └── doc/
│ ├── advanced/
│ │ └── 1-compiling.md
│ ├── index.md
│ └── user-guide/
│ ├── 1-install.md
│ ├── 2-initialization.md
│ ├── 3-using-fmod-plugin.md
│ ├── 4-loading-banks.md
│ ├── 5-playing-events.md
│ ├── 6-listeners.md
│ ├── 7-playing-sounds.md
│ ├── 8-other-low-level-examples.md
│ └── 9-plugins.md
├── get_fmod.py
├── jni/
│ └── Application.mk
└── src/
├── callback/
│ ├── event_callbacks.cpp
│ ├── event_callbacks.h
│ ├── file_callbacks.cpp
│ └── file_callbacks.h
├── constants.h
├── core/
│ ├── fmod_file.cpp
│ ├── fmod_file.h
│ ├── fmod_sound.cpp
│ └── fmod_sound.h
├── data/
│ ├── performance_data.cpp
│ └── performance_data.h
├── fmod_cache.cpp
├── fmod_cache.h
├── fmod_logging.cpp
├── fmod_logging.h
├── fmod_server.cpp
├── fmod_server.h
├── fmod_string_names.cpp
├── fmod_string_names.h
├── helpers/
│ ├── common.h
│ ├── constants.h
│ ├── current_function.h
│ ├── files.h
│ └── maths.h
├── nodes/
│ ├── fmod_bank_loader.cpp
│ ├── fmod_bank_loader.h
│ ├── fmod_event_emitter.h
│ ├── fmod_event_emitter_2d.cpp
│ ├── fmod_event_emitter_2d.h
│ ├── fmod_event_emitter_3d.cpp
│ ├── fmod_event_emitter_3d.h
│ ├── fmod_listener.h
│ ├── fmod_listener_2d.cpp
│ ├── fmod_listener_2d.h
│ ├── fmod_listener_3d.cpp
│ └── fmod_listener_3d.h
├── plugins/
│ ├── ios_plugins_loader.h
│ └── plugins_helper.h
├── register_types.cpp
├── register_types.h
├── resources/
│ ├── fmod_dsp_settings.cpp
│ ├── fmod_dsp_settings.h
│ ├── fmod_logging_settings.cpp
│ ├── fmod_logging_settings.h
│ ├── fmod_plugins_settings.cpp
│ ├── fmod_plugins_settings.h
│ ├── fmod_settings.cpp
│ ├── fmod_settings.h
│ ├── fmod_software_format_settings.cpp
│ ├── fmod_software_format_settings.h
│ ├── fmod_sound_3d_settings.cpp
│ └── fmod_sound_3d_settings.h
├── studio/
│ ├── fmod_bank.cpp
│ ├── fmod_bank.h
│ ├── fmod_bus.cpp
│ ├── fmod_bus.h
│ ├── fmod_event.cpp
│ ├── fmod_event.h
│ ├── fmod_event_description.cpp
│ ├── fmod_event_description.h
│ ├── fmod_parameter_description.cpp
│ ├── fmod_parameter_description.h
│ ├── fmod_vca.cpp
│ └── fmod_vca.h
└── tools/
├── fmod_editor_export_plugin.cpp
├── fmod_editor_export_plugin.h
├── fmod_editor_plugin.cpp
└── fmod_editor_plugin.h
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
---
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AlignEscapedNewlines: Left
AlignOperands: AlignAfterOperator
AlignTrailingComments: false
#Allow
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Always
AllowShortLambdasOnASingleLine: All
AllowShortEnumsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
#Brace`
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
BreakStringLiterals: true
ColumnLimit: 120
CompactNamespaces: false
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: Always
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '".*"'
Priority: 1
- Regex: '^<.*\.h>'
Priority: 2
- Regex: '^<.*'
Priority: 3
IndentAccessModifiers: false
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
NamespaceIndentation: All
PackConstructorInitializers: CurrentLine
#Penalty
PenaltyBreakAssignment: 40
PenaltyBreakBeforeFirstCallParameter: 0
PenaltyBreakComment: 100
PenaltyBreakFirstLessLess: 0
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 100
PenaltyBreakTemplateDeclaration: 0
PenaltyExcessCharacter: 1
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 10000
PointerAlignment: Left
ReferenceAlignment: Left
ReflowComments: true
SeparateDefinitionBlocks: Always
ShortNamespaceLines: 0
SortIncludes: CaseInsensitive
SortUsingDeclarations: false
#Space
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 0
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: 1
SpacesInParentheses: false
SpacesInSquareBrackets: false
#Tab
TabWidth: 4
UseTab: Never
---
Language: Cpp
Standard: c++17
...
================================================
FILE: .gitattributes
================================================
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
# Ignore some files when exporting to a ZIP.
# Only include the addons folder when downloading from the Asset Library.
/** export-ignore
/addons/fmod !export-ignore
/addons/fmod/** !export-ignore
================================================
FILE: .github/actions/create-android-plugin/action.yaml
================================================
name: Create Fmod native build
description: Creates fmod native build for a specific platform
runs:
using: composite
steps:
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Download fmod jar
uses: actions/download-artifact@v4
with:
name: fmod-jar
path: android-plugin/library/libraries/
- name: Build android plugin
uses: eskatos/gradle-command-action@v1
with:
wrapper-directory: android-plugin/
build-root-directory: android-plugin/
arguments: :library:build
================================================
FILE: .github/actions/create-native-build/action.yaml
================================================
name: Create Fmod native build
description: Creates fmod native build for a specific platform
inputs:
platform:
description: The platform to build for.
target:
description: The target to build.
additional-python-packages:
description: Additional python package to install.
flags:
description: Additional compilation scons flags.
fmod-executable-suffix:
description: The suffix of fmod executable to install fmod libraries.
fmod-user:
description: The FMOD user used to download fmod binaries
fmod-password:
description: The password for fmod account used to download fmod binaries.
shell:
description: The shell used by the runner.
runs:
using: composite
steps:
# Use python 3.x release (works cross platform; best to keep self contained in it's own step)
- name: Set up Python 3.x
uses: actions/setup-python@v4
with:
# Semantic version range syntax or exact version of a Python version
python-version: "3.x"
# Optional - x64 or x86 architecture, defaults to x64
architecture: "x64"
# Setup scons, print python version and scons version info, so if anything is broken it won't run the build.
#TODO: remove hardcoded scons version when https://github.com/godotengine/godot-cpp/pull/1526 is released
- name: Configuring Python packages
shell: ${{ inputs.shell }}
run: |
python -c "import sys; print(sys.version)"
python -m pip install scons==4.7.0 requests ${{ inputs.additional-python-packages }}
python --version
scons --version
- name: Installing FMOD on Windows
shell: ${{ inputs.shell }}
if: runner.os == 'Windows'
run: |
cd ..
New-Item -ItemType directory -Path libs; cd libs
New-Item -ItemType directory -Path fmod; cd fmod
python ../../fmod-gdextension/get_fmod.py ${{inputs.fmod-user}} ${{inputs.fmod-password}} ${{inputs.platform}} ${{env.FMOD_VERSION}}
7z x fmodstudioapi${{env.FMOD_VERSION}}${{inputs.fmod-executable-suffix}}
ls
mv api/ windows
cd ../../
- name: Installing FMOD on Linux & Android
if: runner.os == 'Linux'
shell: ${{ inputs.shell }}
run: |
cd ..
mkdir libs && cd libs
mkdir fmod && cd fmod
python ../../fmod-gdextension/get_fmod.py ${{inputs.fmod-user}} ${{inputs.fmod-password}} ${{inputs.platform}} ${{env.FMOD_VERSION}}
tar -xvf fmodstudioapi${{env.FMOD_VERSION}}${{inputs.fmod-executable-suffix}}
mv fmodstudioapi${{env.FMOD_VERSION}}${{inputs.platform}}/api ${{inputs.platform}}
cd ../../
- name: Installing FMOD on MacOS & iOS
if: runner.os == 'MacOS'
shell: ${{ inputs.shell }}
run: |
cd ..
mkdir libs && cd libs
mkdir fmod && cd fmod
python ../../fmod-gdextension/get_fmod.py ${{inputs.fmod-user}} ${{inputs.fmod-password}} ${{inputs.platform}} ${{env.FMOD_VERSION}}
hdiutil attach fmodstudioapi${{env.FMOD_VERSION}}${{inputs.fmod-executable-suffix}}
[[ ${{inputs.platform}} = "macos" ]] && cp -r "/Volumes/FMOD Programmers API Mac/FMOD Programmers API/api" osx
[[ ${{inputs.platform}} = "ios" ]] && cp -r "/Volumes/FMOD Programmers API iOS/FMOD Programmers API/api" ios
cd ../../
- name: create android fmod artifact
if: inputs.platform == 'android' && inputs.target == 'template_release'
shell: ${{ inputs.shell }}
run: |
mkdir android-fmod-artifact
cp ../libs/fmod/android/core/lib/fmod.jar android-fmod-artifact/
cp android-plugin/library/src/main/resources/fmod-android-license.txt android-fmod-artifact/
- name: Upload fmod jar
if: inputs.platform == 'android' && inputs.target == 'template_release'
uses: actions/upload-artifact@v4
with:
name: fmod-jar
path: android-fmod-artifact
if-no-files-found: error
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
- name: Compilation
shell: ${{ inputs.shell }}
run: |
cd ../fmod-gdextension
scons platform=${{ inputs.platform }} generate_bindings=yes target=${{ inputs.target }} target_path=${{ env.TARGET_PATH }} target_name=${{ env.TARGET_NAME }} -j${{ steps.cpu-cores.outputs.count }} ${{ inputs.flags }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.platform }}-${{ inputs.target }}
path: ${{ env.TARGET_PATH }}${{ inputs.platform }}/**/*.*
if-no-files-found: error
================================================
FILE: .github/workflows/check_pr.yml
================================================
name: 🌈 Check Pull Request
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_target:
types: [opened, synchronize, reopened]
# Global Settings
env:
GODOT_VERSION: 4.5
NDK_VERSION: 27.3.13750724
TARGET_PATH: demo/addons/fmod/libs/
TARGET_NAME: libGodotFmod
FMOD_VERSION: 20306
jobs:
gate:
runs-on: ubuntu-latest
# EXCLUSIVE PATHS:
# - In-repo PRs use pull_request
# - External PRs use pull_request_target, so secrets are available, but only after the run is approved
if: >
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
) || (
github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository
)
environment: ${{ github.event_name == 'pull_request_target' && 'external-pr' || '' }}
steps:
- run: echo "Run Approved"
build:
name: ${{ matrix.name }}
needs: gate
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: Windows Editor Compilation
os: "windows-2022"
platform: windows
target: editor
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Windows Debug Compilation
os: "windows-2022"
platform: windows
target: template_debug
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Windows Release Compilation
os: "windows-2022"
platform: windows
target: template_release
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Ubuntu Editor Compilation
os: "ubuntu-22.04"
platform: linux
target: editor
fmod-executable-suffix: linux.tar.gz
fmod-core-platform-folder: linux/core/lib/x86_64
fmod-studio-platform-folder: linux/studio/lib/x86_64
fmod-library-suffix: so
godot-executable-download-suffix: linux.x86_64.zip
godot-executable: Godot_v$GODOT_VERSION-stable_linux.x86_64
shell: bash
- name: Ubuntu Debug Compilation
os: "ubuntu-22.04"
platform: linux
target: template_debug
fmod-executable-suffix: linux.tar.gz
shell: bash
- name: Ubuntu Release Compilation
os: "ubuntu-22.04"
platform: linux
target: template_release
fmod-executable-suffix: linux.tar.gz
shell: bash
- name: MacOS Editor Compilation
os: "macos-14"
platform: macos
target: editor
fmod-executable-suffix: osx.dmg
fmod-core-platform-folder: osx/core/lib
fmod-studio-platform-folder: osx/studio/lib
fmod-library-suffix: dylib
godot-executable-download-suffix: macos.universal.zip
godot-executable: Godot.app/Contents/MacOs/Godot
shell: bash
- name: MacOS Debug Compilation
os: "macos-14"
platform: macos
target: template_debug
fmod-executable-suffix: osx.dmg
shell: bash
- name: MacOS Release Compilation
os: "macos-14"
platform: macos
target: template_release
fmod-executable-suffix: osx.dmg
shell: bash
- name: Android Editor Compilation
os: "ubuntu-22.04"
platform: android
target: editor
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: Android Debug Compilation
os: "ubuntu-22.04"
platform: android
target: template_debug
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: Android Release Compilation
os: "ubuntu-22.04"
platform: android
target: template_release
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: iOS Debug Compilation
os: "macos-14"
platform: ios
target: template_debug
fmod-executable-suffix: ios.dmg
shell: bash
- name: iOS Release Compilation
os: "macos-14"
platform: ios
target: template_release
fmod-executable-suffix: ios.dmg
shell: bash
steps:
- name: Checkout (in-repo PR)
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
lfs: true
submodules: recursive
- name: Checkout (external PR via target)
if: github.event_name == 'pull_request_target'
uses: actions/checkout@v4
with:
lfs: true
submodules: recursive
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Android dependencies
if: ${{ matrix.platform == 'android' }}
uses: nttld/setup-ndk@v1
with:
ndk-version: r23c
link-to-sdk: true
- name: Compile native plugin
uses: ./.github/actions/create-native-build
with:
platform: ${{ matrix.platform }}
target: ${{ matrix.target }}
additional-python-packages: ${{ matrix.additional-python-packages }}
flags: ${{ matrix.flags }}
fmod-executable-suffix: ${{ matrix.fmod-executable-suffix }}
fmod-user: ${{ secrets.FMODUSER }}
fmod-password: ${{ secrets.FMODPASS }}
shell: ${{ matrix.shell }}
- name: Download godot engine
if: matrix.platform != 'android' && matrix.platform != 'ios' && matrix.platform != 'windows' && matrix.target == 'editor'
run: |
wget https://github.com/godotengine/godot-builds/releases/download/${{env.GODOT_VERSION}}-stable/Godot_v${{env.GODOT_VERSION}}-stable_${{ matrix.godot-executable-download-suffix }}
unzip Godot_v${{env.GODOT_VERSION}}-stable_${{ matrix.godot-executable-download-suffix }}
rm Godot_v${{env.GODOT_VERSION}}-stable_${{ matrix.godot-executable-download-suffix }}
- name: Run tests
if: matrix.platform != 'android' && matrix.platform != 'ios' && matrix.platform != 'windows' && matrix.target == 'editor'
run: |
cd demo
chmod +x run_tests.sh
chmod +x ../${{ matrix.godot-executable }}
./run_tests.sh ../${{ matrix.godot-executable }}
create-android-plugin:
needs: [build]
strategy:
matrix:
include:
- os: "ubuntu-22.04"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
lfs: true
submodules: recursive
- name: Create android plugin
uses: ./.github/actions/create-android-plugin
================================================
FILE: .github/workflows/release.yml
================================================
name: 🌈 Release
on:
push:
tags:
- '\d+.\d+.\d+-\d+.\d+.\d+'
# Global Settings
env:
GODOT_VERSION: 4.5
NDK_VERSION: 27.3.13750724
TARGET_PATH: demo/addons/fmod/libs/
TARGET_NAME: libGodotFmod
FMOD_VERSION: 20306
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: Windows Editor Compilation
os: "windows-2022"
platform: windows
target: editor
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Windows Debug Compilation
os: "windows-2022"
platform: windows
target: template_debug
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Windows Release Compilation
os: "windows-2022"
platform: windows
target: template_release
additional-python-packages: pywin32
fmod-executable-suffix: win-installer.exe
shell: pwsh
- name: Ubuntu Editor Compilation
os: "ubuntu-22.04"
platform: linux
target: editor
fmod-executable-suffix: linux.tar.gz
fmod-core-platform-folder: linux/core/lib/x86_64
fmod-studio-platform-folder: linux/studio/lib/x86_64
fmod-library-suffix: so
godot-executable-download-suffix: linux.x86_64.zip
godot-executable: Godot_v$GODOT_VERSION-stable_linux.x86_64
shell: bash
- name: Ubuntu Debug Compilation
os: "ubuntu-22.04"
platform: linux
target: template_debug
fmod-executable-suffix: linux.tar.gz
shell: bash
- name: Ubuntu Release Compilation
os: "ubuntu-22.04"
platform: linux
target: template_release
fmod-executable-suffix: linux.tar.gz
shell: bash
- name: MacOS Editor Compilation
os: "macos-14"
platform: macos
target: editor
fmod-executable-suffix: osx.dmg
fmod-core-platform-folder: osx/core/lib
fmod-studio-platform-folder: osx/studio/lib
fmod-library-suffix: dylib
godot-executable-download-suffix: macos.universal.zip
godot-executable: Godot.app/Contents/MacOs/Godot
shell: bash
- name: MacOS Debug Compilation
os: "macos-14"
platform: macos
target: template_debug
fmod-executable-suffix: osx.dmg
shell: bash
- name: MacOS Release Compilation
os: "macos-14"
platform: macos
target: template_release
fmod-executable-suffix: osx.dmg
shell: bash
- name: Android Editor Compilation
os: "ubuntu-22.04"
platform: android
target: editor
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: Android Debug Compilation
os: "ubuntu-22.04"
platform: android
target: template_debug
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: Android Release Compilation
os: "ubuntu-22.04"
platform: android
target: template_release
fmod-executable-suffix: android.tar.gz
flags: ndk_version=$NDK_VERSION arch=arm64
shell: bash
- name: iOS Debug Compilation
os: "macos-14"
platform: ios
target: template_debug
fmod-executable-suffix: ios.dmg
shell: bash
- name: iOS Release Compilation
os: "macos-14"
platform: ios
target: template_release
fmod-executable-suffix: ios.dmg
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
submodules: recursive
- name: Android dependencies
if: ${{ matrix.platform == 'android' }}
uses: nttld/setup-ndk@v1
with:
ndk-version: r23c
link-to-sdk: true
- name: Compile native plugin
uses: ./.github/actions/create-native-build
with:
platform: ${{ matrix.platform }}
target: ${{ matrix.target }}
additional-python-packages: ${{ matrix.additional-python-packages }}
flags: ${{ matrix.flags }}
fmod-executable-suffix: ${{ matrix.fmod-executable-suffix }}
fmod-user: ${{ secrets.FMODUSER }}
fmod-password: ${{ secrets.FMODPASS }}
shell: ${{ matrix.shell }}
package-godot-addon:
needs: [build]
strategy:
matrix:
include:
- os: "ubuntu-22.04"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
lfs: true
submodules: recursive
- name: Create android plugin
uses: ./.github/actions/create-android-plugin
with:
godot-version: ${{ env.GODOT_VERSION }}
- name: Download linux editor libraries
uses: actions/download-artifact@v4
with:
name: linux-editor
path: demo/addons/fmod/libs/linux/
- name: Download linux template_debug libraries
uses: actions/download-artifact@v4
with:
name: linux-template_debug
path: demo/addons/fmod/libs/linux/
- name: Download linux template_release libraries
uses: actions/download-artifact@v4
with:
name: linux-template_release
path: demo/addons/fmod/libs/linux/
- name: Download windows editor libraries
uses: actions/download-artifact@v4
with:
name: windows-editor
path: demo/addons/fmod/libs/windows/
- name: Download windows template_debug libraries
uses: actions/download-artifact@v4
with:
name: windows-template_debug
path: demo/addons/fmod/libs/windows/
- name: Download windows template_release libraries
uses: actions/download-artifact@v4
with:
name: windows-template_release
path: demo/addons/fmod/libs/windows/
- name: Download macos editor libraries
uses: actions/download-artifact@v4
with:
name: macos-editor
path: demo/addons/fmod/libs/macos/
- name: Download macos template_debug libraries
uses: actions/download-artifact@v4
with:
name: macos-template_debug
path: demo/addons/fmod/libs/macos/
- name: Download macos template_release libraries
uses: actions/download-artifact@v4
with:
name: macos-template_release
path: demo/addons/fmod/libs/macos/
- name: Download iOS template_debug libraries
uses: actions/download-artifact@v4
with:
name: ios-template_debug
path: demo/addons/fmod/libs/iOS/
- name: Download iOS template_release libraries
uses: actions/download-artifact@v4
with:
name: ios-template_release
path: demo/addons/fmod/libs/iOS/
- name: Download android editor libraries
uses: actions/download-artifact@v4
with:
name: android-editor
path: demo/addons/fmod/libs/android/
- name: Download android template_debug libraries
uses: actions/download-artifact@v4
with:
name: android-template_debug
path: demo/addons/fmod/libs/android/
- name: Download android template_release libraries
uses: actions/download-artifact@v4
with:
name: android-template_release
path: demo/addons/fmod/libs/android/
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Zip fmod addon
run: |
cd demo/addons/
zip -r addons.zip fmod -x '*/.gitignore'
- name: Zip demo project (without .godot and GUT)
run: |
cd demo
zip -r demo.zip . \
-x '*/.gitignore' \
'*.zip' \
'.godot/*' '.godot/**' \
'addons/gut/*' 'addons/gut/**' 'addons/gut' \
'.gut*' \
'run_tests.sh' \
'test/' 'test/*' 'test/**' \
- name: Upload Addon Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./demo/addons/addons.zip
asset_name: addons.zip
asset_content_type: application/zip
- name: Upload Demo Project Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./demo/demo.zip
asset_name: demo.zip
asset_content_type: application/zip
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/clion,cmake,scons
# Edit at https://www.gitignore.io/?templates=clion,cmake,scons
### OSX ###
.DS_Store
### Android ###
libs/
obj/
### CLion ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
CMakeLists.txt
bin/
*.os
*.o
*.obj
*.exp
*.lib
# Clion specific
.idea/*
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/sonarlint
### CMake ###
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
### CMake Patch ###
# External projects
*-prefix/
### Visual Studio Generated Files ###
*.vcxproj
*.vcxproj.filters
*.sln
### SCons ###
# for projects that use SCons for building: http://http://www.scons.org/
.sconsign.dblite
# When configure fails, SCons outputs these
config.log
.sconf_temp
# End of https://www.gitignore.io/api/clion,cmake,scons
# uncomment when https://github.com/godotengine/godot/issues/71521 fix is in release.
#demo/.godot
**/*.editor
demo/addons/fmod/libs/android/aar/
*.pdb
.vs
================================================
FILE: .gitmodules
================================================
[submodule "godot-cpp"]
path = godot-cpp
url = https://github.com/godotengine/godot-cpp
================================================
FILE: .readthedocs.yml
================================================
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation with MkDocs
mkdocs:
configuration: docs/mkdocs.yml
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt
================================================
FILE: Android.mk
================================================
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := fmod-core-prebuilt
LOCAL_SRC_FILES := ../libs/fmod/android/core/lib/$(TARGET_ARCH_ABI)/libfmod.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := fmod-studio-prebuilt
LOCAL_SRC_FILES := ../libs/fmod/android/studio/lib/$(TARGET_ARCH_ABI)/libfmodstudio.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := godot-prebuilt
ifeq ($(TARGET_ARCH_ABI),x86)
LOCAL_SRC_FILES := ../godot-cpp/bin/libgodot-cpp.android.release.x86.a
endif
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
LOCAL_SRC_FILES := ../godot-cpp/bin/libgodot-cpp.android.release.armv7.a
endif
ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
LOCAL_SRC_FILES := ../godot-cpp/bin/libgodot-cpp.android.release.arm64v8.a
endif
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libGodotFmod.android.release.$(TARGET_ARCH_ABI)
LOCAL_CPPFLAGS := -std=c++14
LOCAL_CPP_FEATURES := rtti exceptions
LOCAL_LDLIBS := -llog
LOCAL_SRC_FILES := \
src/godot_fmod.cpp \
src/gdlibrary.cpp \
src/callback/file_callbacks.cpp \
src/callback/event_callbacks.cpp \
LOCAL_SHARED_LIBRARIES := \
fmod-core-prebuilt \
fmod-studio-prebuilt
LOCAL_C_INCLUDES := \
../godot-cpp/godot-headers \
../godot-cpp/include/ \
../godot-cpp/include/core \
../godot-cpp/include/gen \
../libs/fmod/android/studio/inc \
../libs/fmod/android/core/inc
LOCAL_STATIC_LIBRARIES := godot-prebuilt
include $(BUILD_SHARED_LIBRARY)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Utopia-Rise and Alex Fonseka
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.
================================================
FILE: README.md
================================================

[](https://github.com/utopia-rise/fmod-gdextension/actions/workflows/release.yml)
[](https://discord.com/invite/u2NM2vTGMn)
---
**Godot 4 GDExtension that integrates with the FMOD Studio API.** [FMOD is an audio engine and middleware solution](https://www.fmod.com/) for interactive audio in games. It has been the audio engine behind many
titles such as **Transistor**, **Into the Breach** and **Celeste**. This Godot extension is used by games such as [Koira](https://dont-nod.com/en/games/koira/).
If you need any help, you can join our [Discord Server](https://discord.com/invite/u2NM2vTGMn).
# Installation
1. [Download Latest Release](https://github.com/utopia-rise/fmod-gdextension/releases/latest)
2. Unpack the `addons/fmod` folder into your `/addons` folder within the Godot project
3. Enable this addon within the Godot settings: `Project > Project Settings > Plugins`
#### Read the [official docs](https://fmod-gdextension.readthedocs.io/en/latest/) to get started with this addon.
# Features
## 🔉 Seamless integration with FMOD
Use FMOD Studio to create bank files this addon will auto-load all events for you inside Godot Engine. Live updating works out of the box.

## 🔉 Dedicated Godot nodes
This GDExtension provides nodes such as `FmodEventEmitter2D`, `FmodEventEmitter3D`, `FmodEventListener2D` and `FmodEventListener3D` that can be used in any Godot scene or GDScript code.

# Contributing
In order to be able to PR this repo from a fork, you need to add `FMODUSER` and `FMODPASS` secrets to your fork repo.
This enables CI to download FMOD api.
Feel free to raise pull requests. We hope you'll enjoy this addon!
## How this extension works
This GDExtension exposes most of the Studio API functions to Godot's GDScript and also provides helpers for performing
common functions like attaching Studio events to Godot nodes and playing 3D/positional audio.
> **Note:** This plugin doesn't provide C# bindings to FMOD. There is technically a C# FMOD API but we choose to develop it as a C++ GDExtension. Any language binding with a auto-binding feature for extensions should be able to use this plugin, which is the case for GDScript. C# doesn't offer this feature yet.
## Continuous Delivery
This project uses [Github Actions](https://github.com/features/actions) to continuously deploy released drivers. If you do not want to use those releases, you
can compile from sources by looking to [compile from sources section](./docs/src/doc/advanced/1-compiling.md). This project uses [SEMVER](https://semver.org/).
# Special Thanks
This project is a forked from [godot-fmod-integration](https://github.com/alexfonseka/godot-fmod-integration)
from [alexfonseka](https://github.com/alexfonseka). We'd like to thank him for the work he did, we simply adapted his
work to GDNative.
# Tested Versions
- **Godot Version:** 4.4 stable
- **FMOD Version:** 2.03
[fmodsingleton]: .README/fmodsingleton.png
[usecustombuild]: .README/usecustombuild.png
================================================
FILE: SConstruct
================================================
#!/usr/bin/env python
import os
import shutil
import subprocess
from SCons.Script import SConscript, ARGUMENTS, Action, Copy
target_path = ARGUMENTS.pop("target_path", "demo/addons/fmod/libs/")
target_name = ARGUMENTS.pop("target_name", "libGodotFmod")
fmod_lib_dir = ARGUMENTS.pop("fmod_lib_dir", "../libs/fmod/")
env = SConscript("godot-cpp/SConstruct")
# Add those directory manually, so we can skip the godot_cpp directory when including headers in C++ files
source_path = [
os.path.join("godot-cpp", "include","godot_cpp"),
os.path.join("godot-cpp", "gen", "include","godot_cpp")
]
env.Append(CPPPATH=[env.Dir(d) for d in source_path])
env.Replace(fmod_lib_dir = fmod_lib_dir)
# For the reference:
# - CCFLAGS are compilation flags shared between C and C++
# - CFLAGS are for C-specific compilation flags
# - CXXFLAGS are for C++-specific compilation flags
# - CPPFLAGS are for pre-processor flags
# - CPPDEFINES are for pre-processor defines
# - LINKFLAGS are for linking flags
# tweak this if you want to use different folders, or more folders, to store your source code in.
env.Append(CPPPATH=["src/"])
sources = [
Glob('src/*.cpp'),
Glob('src/callback/*.cpp'),
Glob('src/core/*.cpp'),
Glob('src/data/*.cpp'),
Glob('src/tools/*.cpp'),
Glob('src/helpers/*.cpp'),
Glob('src/nodes/*.cpp'),
Glob('src/resources/*.cpp'),
Glob('src/studio/*.cpp'),
Glob('src/plugins/*.cpp')
]
lfix = ""
debug = False
if env["target"] == "template_debug" or env["target"] == "editor":
lfix = "L"
debug = True
if env["platform"] == "macos":
libfmod = 'libfmod%s.dylib' % lfix
libfmodstudio = 'libfmodstudio%s.dylib' % lfix
env.Append(CPPPATH=[env['fmod_lib_dir'] + 'osx/core/inc/', env['fmod_lib_dir'] + 'osx/studio/inc/'])
env.Append(LIBPATH=[env['fmod_lib_dir'] + 'osx/core/lib/', env['fmod_lib_dir'] + 'osx/studio/lib/'])
env.Append(LIBS=[libfmod, libfmodstudio])
env.Append(
LINKFLAGS=[
"-framework",
"Cocoa",
"-Wl,-undefined,dynamic_lookup",
"-rpath", "@loader_path/.."
]
)
elif env["platform"] == "linux":
libfmod = 'libfmod%s.so'% lfix
libfmodstudio = 'libfmodstudio%s.so'% lfix
env.Append(CPPPATH=[env['fmod_lib_dir'] + 'linux/core/inc/', env['fmod_lib_dir'] + 'linux/studio/inc/'])
env.Append(LIBPATH=[env['fmod_lib_dir'] + 'linux/core/lib/' + env["arch"], env['fmod_lib_dir'] + 'linux/studio/lib/' + env["arch"]])
env.Append(LIBS=[libfmod, libfmodstudio])
env.Append(CCFLAGS=["-fPIC", "-Wwrite-strings"])
env.Append(LINKFLAGS=["-Wl,-R,'$$ORIGIN'"])
env.Append(LINKFLAGS=["-m64", "-fuse-ld=gold"])
elif env["platform"] == "windows":
libfmod = 'fmod%s_vc'% lfix
libfmodstudio = 'fmodstudio%s_vc'% lfix
fmod_info_table = {
"x86_64" : "x64",
"x86_32" : "x86",
}
arch_suffix_override = fmod_info_table[env["arch"]]
env.Append(CPPPATH=[env['fmod_lib_dir'] + 'windows/core/inc/', env['fmod_lib_dir'] + 'windows/studio/inc/'])
env.Append(LIBPATH=[env['fmod_lib_dir'] + 'windows/core/lib/' + arch_suffix_override, env['fmod_lib_dir'] + 'windows/studio/lib/' + arch_suffix_override])
env.Append(LIBS=[libfmod, libfmodstudio])
env.Append(LINKFLAGS=["/WX"])
if debug:
env.Append(CCFLAGS=["/FS", "/Zi"])
elif env["platform"] == "ios":
libfmod = 'libfmod%s_iphoneos.a' % lfix
libfmodstudio = 'libfmodstudio%s_iphoneos.a' % lfix
env.Append(CPPPATH=[env['fmod_lib_dir'] + 'ios/core/inc/', env['fmod_lib_dir'] + 'ios/studio/inc/'])
env.Append(LIBPATH=[env['fmod_lib_dir'] + 'ios/core/lib/', env['fmod_lib_dir'] + 'ios/studio/lib/'])
env.Append(LIBS=[libfmod, libfmodstudio])
env.Append(LINKFLAGS=[
'-Wl,-undefined,dynamic_lookup', "-miphoneos-version-min=" + env["ios_min_version"]
])
elif env["platform"] == "android":
libfmod = 'libfmod%s.so' % lfix
libfmodstudio = 'libfmodstudio%s.so' % lfix
fmod_info_table = {
"armv7": "armeabi-v7a",
"arm64": "arm64-v8a",
"x86": "x86",
"x86_64": "x86_64"
}
arch_dir = fmod_info_table[env["arch"]]
env.Append(CPPPATH=[env['fmod_lib_dir'] + 'android/core/inc/', env['fmod_lib_dir'] + 'android/studio/inc/'])
env.Append(LIBPATH=[env['fmod_lib_dir'] + 'android/core/lib/' + arch_dir, env['fmod_lib_dir'] + 'android/studio/lib/' + arch_dir])
env.Append(LIBS=[libfmod, libfmodstudio])
#Output is placed in the addons directory of the demo project directly
target = "{}{}/{}.{}.{}".format(
target_path, env["platform"], target_name, env["platform"], env["target"]
) if env["platform"] != "android" else "{}{}/{}/{}.{}.{}".format(
target_path, env["platform"], env["arch"], target_name, env["platform"], env["target"]
)
if env["platform"] == "macos":
target = "{}.framework/{}.{}.{}".format(
target,
target_name,
env["platform"],
env["target"]
)
else:
target = "{}.{}{}".format(
target,
env["arch"],
env["SHLIBSUFFIX"]
)
library = env.SharedLibrary(target=target, source=sources)
def sys_exec(args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, text=True)
(out, err) = proc.communicate()
return out.rstrip("\r\n").lstrip()
if env["platform"] == "ios":
xcframework_path = "{}{}/{}.{}.{}.xcframework".format(
target_path,
env["platform"],
target_name,
env["platform"],
env["target"]
)
def create_xcframework(self, arg, env, executor = None):
sys_exec(["xcodebuild", "-create-xcframework", "-library", target, "-output", xcframework_path])
sys_exec(["rm", target])
sys_exec(["/usr/libexec/PlistBuddy", "-c", "Add :MinimumOSVersion string " + env["ios_min_version"], "{}/Info.plist".format(xcframework_path)])
create_xcframework_action = Action('', create_xcframework)
AddPostAction(library, create_xcframework_action)
def copy_fmod_libraries(self, arg, env, executor = None):
fmod_core_lib_dir = ""
fmod_studio_lib_dir = ""
addon_fmod_libs_output = "{}{}/".format(
target_path, env["platform"]
) if env["platform"] != "android" else "{}{}/{}/".format(
target_path, env["platform"], env["arch"]
)
if env["platform"] == "macos":
fmod_core_lib_dir = env['fmod_lib_dir'] + 'osx/core/lib/'
fmod_studio_lib_dir = env['fmod_lib_dir'] + 'osx/studio/lib/'
elif env["platform"] == "linux":
fmod_core_lib_dir = env['fmod_lib_dir'] + 'linux/core/lib/' + env["arch"]
fmod_studio_lib_dir = env['fmod_lib_dir'] + 'linux/studio/lib/' + env["arch"]
elif env["platform"] == "windows":
fmod_core_lib_dir = env['fmod_lib_dir'] + 'windows/core/lib/' + arch_suffix_override + '/'
fmod_studio_lib_dir = env['fmod_lib_dir'] + 'windows/studio/lib/' + arch_suffix_override + '/'
elif env["platform"] == "ios":
fmod_core_lib_dir = env['fmod_lib_dir'] + 'ios/core/lib/'
fmod_studio_lib_dir = env['fmod_lib_dir'] + 'ios/studio/lib/'
elif env["platform"] == "android":
fmod_core_lib_dir = env['fmod_lib_dir'] + 'android/core/lib/' + arch_dir
fmod_studio_lib_dir = env['fmod_lib_dir'] + 'android/studio/lib/' + arch_dir
source_files = [env.Glob(os.path.join(source_dir, '*.*')) for source_dir in [fmod_core_lib_dir, fmod_studio_lib_dir]]
[[shutil.copy(str(file), addon_fmod_libs_output) for file in files] for files in source_files]
copy_fmod_libraries_action = Action('', copy_fmod_libraries)
AddPostAction(library, copy_fmod_libraries_action)
Default(library)
================================================
FILE: android-plugin/.gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
library/libraries/
================================================
FILE: android-plugin/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.library") version "8.0.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.20" apply false
}
================================================
FILE: android-plugin/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: android-plugin/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: android-plugin/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: android-plugin/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: android-plugin/library/.gitignore
================================================
/build
================================================
FILE: android-plugin/library/build.gradle.kts
================================================
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
val pluginName = "fmod"
val pluginPackageName = "com.utopiarise.godot.fmod.android.plugin"
android {
namespace = pluginPackageName
compileSdk = 33
defaultConfig {
minSdk = 24
targetSdk = 33
manifestPlaceholders["godotPluginName"] = pluginName
manifestPlaceholders["godotPluginPackageName"] = pluginPackageName
buildConfigField("String", "GODOT_PLUGIN_NAME", "\"${pluginName}\"")
setProperty("archivesBaseName", pluginName)
}
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = false
buildConfigField("boolean", "DEBUG", "false")
}
debug {
buildConfigField("boolean", "DEBUG", "true")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
val copyDebugAARToDemoAddons by tasks.registering(Copy::class) {
description = "Copies the generated debug AAR binary to the plugin's addons directory"
from("build/outputs/aar")
include("$pluginName-debug.aar")
into("../../demo/addons/$pluginName/libs/android")
}
val copyReleaseAARToDemoAddons by tasks.registering(Copy::class) {
description = "Copies the generated release AAR binary to the plugin's addons directory"
from("build/outputs/aar")
include("$pluginName-release.aar")
into("../../demo/addons/$pluginName/libs/android")
}
tasks.named("assemble").configure {
finalizedBy(copyDebugAARToDemoAddons)
finalizedBy(copyReleaseAARToDemoAddons)
}
dependencies {
implementation(files("libraries/fmod.jar"))
implementation("org.godotengine:godot:4.5.0.stable")
}
================================================
FILE: android-plugin/library/src/main/AndroidManifest.xml
================================================
================================================
FILE: android-plugin/library/src/main/kotlin/com/utopiarise/godot/fmod/android/plugin/FmodPlugin.kt
================================================
package com.utopiarise.godot.fmod.android.plugin
import android.app.Activity
import android.view.View
import org.fmod.FMOD
import org.godotengine.godot.Godot
import org.godotengine.godot.plugin.GodotPlugin
class FmodPlugin(godot: Godot) : GodotPlugin(godot) {
override fun getPluginName() = "Godot Fmod Android Plugin"
override fun onMainCreate(activity: Activity?): View? {
FMOD.init(activity)
return super.onMainCreate(activity)
}
override fun onMainDestroy() {
FMOD.close()
}
companion object {
init {
if (BuildConfig.DEBUG) {
System.loadLibrary("fmodL")
System.loadLibrary("fmodstudioL")
} else {
System.loadLibrary("fmod")
System.loadLibrary("fmodstudio")
}
}
}
}
================================================
FILE: android-plugin/library/src/main/resources/fmod-android-license.txt
================================================
Copyright (C) 2010 The Android Open Source Project All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following
disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: android-plugin/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Godot Fmod Android Plugin"
include(":library")
================================================
FILE: demo/.gitattributes
================================================
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
================================================
FILE: demo/.gitignore
================================================
# Godot 4+ specific ignores
.godot/
# Godot-specific ignores
.import/
export.cfg
export_credentials.cfg
# Imported translations (automatically generated from CSV files)
*.translation
# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json
.vscode/
logs/
**/*.aar
android/build
android/
================================================
FILE: demo/.gutconfig.json
================================================
{
"background_color": "262626ff",
"compact_mode": false,
"configured_dirs": [
"res://test/unit"
],
"dirs": [
"res://test/unit"
],
"disable_colors": false,
"double_strategy": 1,
"failure_error_types": [
"gut"
],
"font_color": "ccccccff",
"font_name": "CourierPrime",
"font_size": 16.0,
"gut_on_top": true,
"hide_orphans": false,
"ignore_pause": false,
"include_subdirs": false,
"junit_xml_file": "",
"junit_xml_timestamp": false,
"log_level": 1.0,
"no_error_tracking": false,
"opacity": 100.0,
"paint_after": 0.1,
"post_run_script": "",
"pre_run_script": "",
"prefix": "test_",
"should_exit": true,
"should_exit_on_success": false,
"should_maximize": false,
"suffix": ".gd",
"wait_log_delay": 0.5
}
================================================
FILE: demo/addons/fmod/.gitignore
================================================
*.dll
*.a
*.dylib
*.so
*.so.*
*.xcframework
!libs/
================================================
FILE: demo/addons/fmod/FmodAndroidExportPlugin.gd
================================================
@tool
class_name FmodAndroidExportPlugin extends EditorExportPlugin
var plugin_name: String = "fmod"
func _supports_platform(platform):
if platform is EditorExportPlatformAndroid:
return true
return false
func _get_android_libraries(platform, debug):
if debug:
return PackedStringArray([plugin_name + "/libs/android/" + plugin_name + "-debug.aar"])
else:
return PackedStringArray([plugin_name + "/libs/android/" + plugin_name + "-release.aar"])
func _get_name():
return plugin_name
================================================
FILE: demo/addons/fmod/FmodAndroidExportPlugin.gd.uid
================================================
uid://cle6f2srp4yun
================================================
FILE: demo/addons/fmod/FmodManager.gd
================================================
@tool
extends Node
var performance_display: PerformancesDisplay
func _ready():
process_mode = PROCESS_MODE_ALWAYS
performance_display = PerformancesDisplay.new()
add_child(performance_display)
func _exit_tree() -> void:
remove_child(performance_display)
performance_display.free()
func _process(delta):
FmodServer.update()
func _notification(what):
FmodServer.notification(what)
if OS.has_feature("mobile"):
match what:
NOTIFICATION_APPLICATION_FOCUS_OUT: FmodServer.mixer_suspend()
NOTIFICATION_APPLICATION_FOCUS_IN: FmodServer.mixer_resume()
================================================
FILE: demo/addons/fmod/FmodManager.gd.uid
================================================
uid://cds10pm7hwn3p
================================================
FILE: demo/addons/fmod/FmodPlugin.gd
================================================
@tool
class_name FmodPlugin extends EditorPlugin
const ADDON_PATH: StringName = &"res://addons/fmod"
const FmodManager_Autoload_Name: StringName = &"FmodManager"
@onready var theme: Theme = get_editor_interface().get_base_control().get_theme()
var fmod_bank_explorer_window: PackedScene = load("res://addons/fmod/tool/ui/FmodBankExplorer.tscn")
var bank_explorer: FmodBankExplorer
var fmod_button: Button
var export_plugin: FmodEditorExportPlugin = FmodEditorExportPlugin.new()
var android_export_plugin: FmodAndroidExportPlugin = FmodAndroidExportPlugin.new()
var emitter_inspector_plugin: FmodEmitterPropertyInspectorPlugin = FmodEmitterPropertyInspectorPlugin.new(self)
var bank_loader_inspector_plugin: FmodBankLoaderPropertyInspectorPlugin = FmodBankLoaderPropertyInspectorPlugin.new(self)
func _init() -> void:
FmodBankDatabase.reload_all_banks()
func _enable_plugin() -> void:
add_autoload_singleton(FmodManager_Autoload_Name, "res://addons/fmod/FmodManager.gd")
func _disable_plugin() -> void:
remove_autoload_singleton(FmodManager_Autoload_Name)
func _enter_tree() -> void:
_add_explorer_button()
_add_bank_explorer_window()
add_inspector_plugin(bank_loader_inspector_plugin)
add_inspector_plugin(emitter_inspector_plugin)
add_export_plugin(export_plugin)
add_export_plugin(android_export_plugin)
func _exit_tree() -> void:
remove_control_from_container(EditorPlugin.CONTAINER_TOOLBAR, fmod_button)
fmod_button.queue_free()
bank_explorer.queue_free()
remove_inspector_plugin(emitter_inspector_plugin)
remove_inspector_plugin(bank_loader_inspector_plugin)
remove_export_plugin(android_export_plugin)
remove_export_plugin(export_plugin)
func _add_explorer_button() -> void:
fmod_button = Button.new()
fmod_button.icon = load("res://addons/fmod/icons/fmod_icon.svg")
fmod_button.text = "Fmod Explorer"
fmod_button.pressed.connect(_on_project_explorer_button_clicked)
add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, fmod_button)
func _add_bank_explorer_window() -> void:
bank_explorer = fmod_bank_explorer_window.instantiate()
bank_explorer.theme = get_editor_interface().get_base_control().get_theme()
bank_explorer.visible = false
add_child(bank_explorer)
func _on_project_explorer_button_clicked() -> void:
bank_explorer.should_display_copy_buttons = true
bank_explorer.should_display_select_button = false
_popup_project_explorer(FmodBankExplorer.ToDisplayFlags.BUSES | FmodBankExplorer.ToDisplayFlags.VCA | FmodBankExplorer.ToDisplayFlags.EVENTS)
func open_project_explorer_events(on_select_callable: Callable) -> void:
_open_project_explorer(FmodBankExplorer.ToDisplayFlags.EVENTS, on_select_callable)
func open_project_explorer_bank(on_select_callable: Callable) -> void:
_open_project_explorer(0, on_select_callable)
func _open_project_explorer(display_flag: int, on_select_callable: Callable) -> void:
bank_explorer.should_display_copy_buttons = false
bank_explorer.should_display_select_button = true
_popup_project_explorer(display_flag, on_select_callable)
func _popup_project_explorer(to_display: int, callable: Callable = Callable()) -> void:
if bank_explorer.visible == true:
bank_explorer.close_window()
return
bank_explorer.flags = to_display
bank_explorer.reset_search()
bank_explorer.regenerate_tree(callable)
bank_explorer.popup_centered()
================================================
FILE: demo/addons/fmod/FmodPlugin.gd.uid
================================================
uid://cwsif6rhp50p5
================================================
FILE: demo/addons/fmod/fmod.gdextension
================================================
[configuration]
entry_symbol = "fmod_library_init"
compatibility_minimum = 4.5
[libraries]
windows.editor.x86_64 = "res://addons/fmod/libs/windows/libGodotFmod.windows.editor.x86_64.dll"
windows.debug.x86_64 = "res://addons/fmod/libs/windows/libGodotFmod.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://addons/fmod/libs/windows/libGodotFmod.windows.template_release.x86_64.dll"
macos.editor = "res://addons/fmod/libs/macos/libGodotFmod.macos.editor.framework"
macos.debug = "res://addons/fmod/libs/macos/libGodotFmod.macos.template_debug.framework"
macos.release = "res://addons/fmod/libs/macos/libGodotFmod.macos.template_release.framework"
linux.editor.x86_64 = "res://addons/fmod/libs/linux/libGodotFmod.linux.editor.x86_64.so"
linux.debug.x86_64 = "res://addons/fmod/libs/linux/libGodotFmod.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://addons/fmod/libs/linux/libGodotFmod.linux.template_release.x86_64.so"
android.debug.x86_64 = "res://addons/fmod/libs/android/x86_64/libGodotFmod.android.template_debug.x86_64.so"
android.release.x86_64 = "res://addons/fmod/libs/android/x86_64/libGodotFmod.android.template_release.x86_64.so"
android.debug.arm64 = "res://addons/fmod/libs/android/arm64/libGodotFmod.android.template_debug.arm64.so"
android.release.arm64 = "res://addons/fmod/libs/android/arm64/libGodotFmod.android.template_release.arm64.so"
ios.debug = "res://addons/fmod/libs/ios/libGodotFmod.ios.template_debug.xcframework"
ios.release = "res://addons/fmod/libs/ios/libGodotFmod.ios.template_release.xcframework"
[icons]
FmodEventEmitter2D = "res://addons/fmod/icons/fmod_icon.svg"
FmodEventEmitter3D = "res://addons/fmod/icons/fmod_icon.svg"
FmodListener2D = "res://addons/fmod/icons/fmod_icon.svg"
FmodListener3D = "res://addons/fmod/icons/fmod_icon.svg"
FmodBankLoader = "res://addons/fmod/icons/fmod_icon.svg"
[dependencies]
windows.editor.x86_64 = {
"libs/windows/fmodL.dll": "",
"libs/windows/fmodstudioL.dll": ""
}
windows.debug.x86_64 = {
"libs/windows/fmodL.dll": "",
"libs/windows/fmodstudioL.dll": ""
}
windows.release.x86_64 = {
"libs/windows/fmod.dll": "",
"libs/windows/fmodstudio.dll": ""
}
linux.editor.x86_64 = {
"libs/linux/libfmodL.so": "",
"libs/linux/libfmodL.so.14": "",
"libs/linux/libfmodL.so.14.6": "",
"libs/linux/libfmodstudioL.so": "",
"libs/linux/libfmodstudioL.so.14": "",
"libs/linux/libfmodstudioL.so.14.6": ""
}
linux.debug.x86_64 = {
"libs/linux/libfmodL.so": "",
"libs/linux/libfmodL.so.14": "",
"libs/linux/libfmodL.so.14.6": "",
"libs/linux/libfmodstudioL.so": "",
"libs/linux/libfmodstudioL.so.14": "",
"libs/linux/libfmodstudioL.so.14.6": ""
}
linux.release.x86_64 = {
"libs/linux/libfmod.so": "",
"libs/linux/libfmod.so.14": "",
"libs/linux/libfmod.so.14.6": "",
"libs/linux/libfmodstudio.so": "",
"libs/linux/libfmodstudio.so.14": "",
"libs/linux/libfmodstudio.so.14.6": ""
}
macos.editor = {
"libs/macos/libfmodL.dylib": "",
"libs/macos/libfmodstudioL.dylib": ""
}
macos.debug = {
"libs/macos/libfmodL.dylib": "",
"libs/macos/libfmodstudioL.dylib": ""
}
macos.release = {
"libs/macos/libfmod.dylib": "",
"libs/macos/libfmodstudio.dylib": ""
}
android.debug.x86_64 = {
"libs/android/x86_64/libfmodL.so": "",
"libs/android/x86_64/libfmodstudioL.so": "",
}
android.release.x86_64 = {
"libs/android/x86_64/libfmod.so": "",
"libs/android/x86_64/libfmodstudio.so": "",
}
android.debug.arm64 = {
"libs/android/arm64/libfmodL.so": "",
"libs/android/arm64/libfmodstudioL.so": "",
}
android.release.arm64 = {
"libs/android/arm64/libfmod.so": "",
"libs/android/arm64/libfmodstudio.so": "",
}
ios.debug = {
"libs/ios/libfmodL_iphoneos.a": "",
"libs/ios/libfmodstudioL_iphoneos.a": ""
}
ios.release = {
"libs/ios/libfmod_iphoneos.a": "",
"libs/ios/libfmodstudio_iphoneos.a": ""
}
================================================
FILE: demo/addons/fmod/fmod.gdextension.uid
================================================
uid://dq17s7r52klxe
================================================
FILE: demo/addons/fmod/icons/bank_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://o2chsr07oeqs"
path="res://.godot/imported/bank_icon.svg-8de6c7bff09a67352e162b3c61b601de.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/bank_icon.svg"
dest_files=["res://.godot/imported/bank_icon.svg-8de6c7bff09a67352e162b3c61b601de.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=true
================================================
FILE: demo/addons/fmod/icons/bus_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dj1kag4aeg58t"
path="res://.godot/imported/bus_icon.svg-f536ffd3c4893e79a9d3cb9a1b4fb50c.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/bus_icon.svg"
dest_files=["res://.godot/imported/bus_icon.svg-f536ffd3c4893e79a9d3cb9a1b4fb50c.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=true
================================================
FILE: demo/addons/fmod/icons/c_parameter_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmvcqfsl167te"
path="res://.godot/imported/c_parameter_icon.svg-04115c2482c9a6874356ffdc09c41db0.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/c_parameter_icon.svg"
dest_files=["res://.godot/imported/c_parameter_icon.svg-04115c2482c9a6874356ffdc09c41db0.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=true
================================================
FILE: demo/addons/fmod/icons/d_parameter_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgna04txtwnyb"
path="res://.godot/imported/d_parameter_icon.svg-d339e4e3f950ae8593b999ef51579136.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/d_parameter_icon.svg"
dest_files=["res://.godot/imported/d_parameter_icon.svg-d339e4e3f950ae8593b999ef51579136.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=true
================================================
FILE: demo/addons/fmod/icons/event_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmpgmbn3y4svl"
path="res://.godot/imported/event_icon.svg-4e6e2103d076f95b7bef82f079e433e6.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/event_icon.svg"
dest_files=["res://.godot/imported/event_icon.svg-4e6e2103d076f95b7bef82f079e433e6.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=true
================================================
FILE: demo/addons/fmod/icons/fmod_emitter.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cotpb54utx6d6"
path="res://.godot/imported/fmod_emitter.png-6783a287e298e2a04e64a6deaa6fe366.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/fmod_emitter.png"
dest_files=["res://.godot/imported/fmod_emitter.png-6783a287e298e2a04e64a6deaa6fe366.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
================================================
FILE: demo/addons/fmod/icons/fmod_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bwqq5q7kodk40"
path="res://.godot/imported/fmod_icon.svg-cca7eb13231881fafaea0d598d407ea3.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/fmod_icon.svg"
dest_files=["res://.godot/imported/fmod_icon.svg-cca7eb13231881fafaea0d598d407ea3.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=true
================================================
FILE: demo/addons/fmod/icons/snapshot_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b4jxbh86chubi"
path="res://.godot/imported/snapshot_icon.svg-7b517248819b3685844766808fbce2a5.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/snapshot_icon.svg"
dest_files=["res://.godot/imported/snapshot_icon.svg-7b517248819b3685844766808fbce2a5.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=true
================================================
FILE: demo/addons/fmod/icons/vca_icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crsj4jjaeq87a"
path="res://.godot/imported/vca_icon.svg-def43f27fe148a7a0b18c7dcaab20c79.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/fmod/icons/vca_icon.svg"
dest_files=["res://.godot/imported/vca_icon.svg-def43f27fe148a7a0b18c7dcaab20c79.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=true
================================================
FILE: demo/addons/fmod/libs/android/arm64/CopyPast_Fmod_Libs_Here.txt
================================================
================================================
FILE: demo/addons/fmod/libs/iOS/CopyPast_Fmod_Libs_Here.txt
================================================
================================================
FILE: demo/addons/fmod/libs/linux/CopyPast_Fmod_Libs_Here.txt
================================================
================================================
FILE: demo/addons/fmod/libs/macos/CopyPast_Fmod_Libs_Here.txt
================================================
================================================
FILE: demo/addons/fmod/libs/macos/libGodotFmod.macos.editor.framework/Info.plist
================================================
CFBundleExecutable
libGodotFmod.macos.editor
CFBundleIdentifier
com.utopia-rise.fmod-gdextension
CFBundleInfoDictionaryVersion
6.0
CFBundleName
libGodotFmod.macos.editor
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0.0
CFBundleSupportedPlatforms
MacOSX
CFBundleVersion
1.0.0
LSMinimumSystemVersion
10.12
================================================
FILE: demo/addons/fmod/libs/macos/libGodotFmod.macos.template_debug.framework/Info.plist
================================================
CFBundleExecutable
libGodotFmod.macos.template_debug
CFBundleIdentifier
com.utopiarise.fmod-gdextension
CFBundleInfoDictionaryVersion
6.0
CFBundleName
libGodotFmod.macos.template_debug
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0.0
CFBundleSupportedPlatforms
MacOSX
CFBundleVersion
1.0.0
LSMinimumSystemVersion
10.12
================================================
FILE: demo/addons/fmod/libs/macos/libGodotFmod.macos.template_release.framework/Info.plist
================================================
CFBundleExecutable
libGodotFmod.macos.template_release
CFBundleIdentifier
com.utopiarise.fmod-gdextension
CFBundleInfoDictionaryVersion
6.0
CFBundleName
libGodotFmod.macos.template_release
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0.0
CFBundleSupportedPlatforms
MacOSX
CFBundleVersion
1.0.0
LSMinimumSystemVersion
10.12
================================================
FILE: demo/addons/fmod/libs/windows/CopyPast_Fmod_Libs_Here.txt
================================================
================================================
FILE: demo/addons/fmod/plugin.cfg
================================================
[plugin]
name="FMOD GDExtension"
description="FMOD GDExtension for Godot Engine 4.5"
author="Utopia-rise, Tristan Grespinet, Pierre-Thomas Meisels"
version="6.1.0"
script="FmodPlugin.gd"
================================================
FILE: demo/addons/fmod/tool/FmodBankDatabase.gd
================================================
extends Node
class_name FmodBankDatabase
static var banks := Array()
const MASTER_STRINGS_BANK_NAME = "Master.strings.bank"
const MASTER_BANK_NAME = "Master.bank"
static func reload_all_banks():
banks.clear()
var banks_root = ProjectSettings.get_setting("Fmod/General/banks_path", "")
var master_strings_bank_path = "%s/%s" % [banks_root, MASTER_STRINGS_BANK_NAME]
var master_bank_path = "%s/%s" % [banks_root, MASTER_BANK_NAME]
if not FileAccess.file_exists(master_strings_bank_path):
push_warning("Cannot find master strings bank at %s" % master_strings_bank_path)
return
if not FileAccess.file_exists(master_bank_path):
push_warning("Cannot find master bank at %s" % master_bank_path)
return
banks.append(
FmodServer.load_bank(master_strings_bank_path, FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
)
banks.append(
FmodServer.load_bank(master_bank_path, FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
)
var dir: DirAccess = DirAccess.open(banks_root)
if dir:
dir.list_dir_begin()
var file_name : String = dir.get_next()
while file_name != "":
if dir.current_is_dir():
pass # the found item is a directory
elif file_name.ends_with(".bank") and file_name != MASTER_STRINGS_BANK_NAME and file_name != MASTER_BANK_NAME:
banks.append(
FmodServer.load_bank("%s/%s" % [banks_root, file_name], FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)
)
file_name = dir.get_next()
================================================
FILE: demo/addons/fmod/tool/FmodBankDatabase.gd.uid
================================================
uid://dovuikgbkylhi
================================================
FILE: demo/addons/fmod/tool/inspectors/FmodBankLoaderPropertyInspectorPlugin.gd
================================================
class_name FmodBankLoaderPropertyInspectorPlugin extends EditorInspectorPlugin
static var bank_icon = load("res://addons/fmod/icons/bank_icon.svg")
var _open_project_explorer_callable: Callable
func _init(plugin: FmodPlugin):
_open_project_explorer_callable = plugin.open_project_explorer_bank
func _can_handle(object: Object):
return object is FmodBankLoader
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool):
return name == "bank_paths"
func _parse_category(object: Object, category: String):
if category != "FmodBankLoader":
return
var editor_property := FmodBankPathEditorProperty.new(_open_project_explorer_callable)
add_property_editor("bank_paths", editor_property, false, "Fmod banks")
================================================
FILE: demo/addons/fmod/tool/inspectors/FmodBankLoaderPropertyInspectorPlugin.gd.uid
================================================
uid://tnljjxahubam
================================================
FILE: demo/addons/fmod/tool/inspectors/FmodEmitterPropertyInspectorPlugin.gd
================================================
class_name FmodEmitterPropertyInspectorPlugin extends EditorInspectorPlugin
var _open_project_explorer_callable: Callable
var _event_editor_property_scene: PackedScene = load("res://addons/fmod/tool/property_editors/FmodEventEditorProperty.tscn")
func _init(plugin: FmodPlugin):
_open_project_explorer_callable = plugin.open_project_explorer_events
func _can_handle(object: Object):
return object is FmodEventEmitter2D or \
object is FmodEventEmitter3D
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool):
return name == "event_name" || name == "event_guid"
func _parse_category(object: Object, category: String):
if category != "FmodEventEmitter2D" and category != "FmodEventEmitter3D":
return
var editor_property := _event_editor_property_scene.instantiate()
editor_property.initialize(_open_project_explorer_callable, "event_name", "event_guid")
add_property_editor_for_multiple_properties("Fmod event", PackedStringArray(["event_name", "event_guid"]), editor_property)
================================================
FILE: demo/addons/fmod/tool/inspectors/FmodEmitterPropertyInspectorPlugin.gd.uid
================================================
uid://co1ktq45h26wx
================================================
FILE: demo/addons/fmod/tool/performances/PerformancesDisplay.gd
================================================
class_name PerformancesDisplay extends Node
const CORE_CPU_DSP_CATEGORY = "FMOD [Core]/Cpu DSP"
const CORE_CPU_GEOMETRY_CATEGORY = "FMOD [Core]/Cpu Geometry"
const CORE_CPU_STREAM_CATEGORY = "FMOD [Core]/Cpu Stream"
const CORE_CPU_UPDATE_CATEGORY = "FMOD/[Core] Cpu Update"
const CORE_CPU_CONVOLUTION_THREAD1_CATEGORY = "FMOD/[Core] Cpu convolution Thread 1"
const CORE_CPU_CONVOLUTION_THREAD2_CATEGORY = "FMOD/[Core] Cpu convolution Thread 2"
const STUDIO_CPU_UPDATE_CATEGORY = "FMOD/[Studio] Cpu Update"
const MEMORY_CURRENTLY_ALLOCATED_CATEGORY = "FMOD/[Memory] Currently allocated"
const MEMORY_MAX_ALLOCATED_CATEGORY = "FMOD/[Memory] Max allocated"
const FILE_SAMPLE_CATEGORY = "FMOD/[File] Sample bytes read"
const FILE_STREAM_CATEGORY = "FMOD/[File] Stream bytes read"
const FILE_OTHER_CATEGORY = "FMOD/[File] Other bytes read"
func _enter_tree():
var performance_data: FmodPerformanceData = FmodServer.get_performance_data()
add_monitor(CORE_CPU_DSP_CATEGORY, func(): return performance_data.dsp)
add_monitor(CORE_CPU_GEOMETRY_CATEGORY, func(): return performance_data.geometry)
add_monitor(CORE_CPU_STREAM_CATEGORY, func(): return performance_data.stream)
add_monitor(CORE_CPU_UPDATE_CATEGORY, func(): return performance_data.update)
add_monitor(CORE_CPU_CONVOLUTION_THREAD1_CATEGORY, func(): return performance_data.convolution1)
add_monitor(CORE_CPU_CONVOLUTION_THREAD2_CATEGORY, func(): return performance_data.convolution2)
add_monitor(STUDIO_CPU_UPDATE_CATEGORY, func(): return performance_data.studio)
add_monitor(MEMORY_CURRENTLY_ALLOCATED_CATEGORY, func(): return performance_data.currently_allocated)
add_monitor(MEMORY_MAX_ALLOCATED_CATEGORY, func(): return performance_data.max_allocated)
add_monitor(FILE_SAMPLE_CATEGORY, func(): return performance_data.sample_bytes_read)
add_monitor(FILE_STREAM_CATEGORY, func(): return performance_data.stream_bytes_read)
add_monitor(FILE_OTHER_CATEGORY, func(): return performance_data.other_bytes_read)
func _exit_tree() -> void:
remove_monitor(CORE_CPU_DSP_CATEGORY)
remove_monitor(CORE_CPU_GEOMETRY_CATEGORY)
remove_monitor(CORE_CPU_STREAM_CATEGORY)
remove_monitor(CORE_CPU_UPDATE_CATEGORY)
remove_monitor(CORE_CPU_CONVOLUTION_THREAD1_CATEGORY)
remove_monitor(CORE_CPU_CONVOLUTION_THREAD2_CATEGORY)
remove_monitor(STUDIO_CPU_UPDATE_CATEGORY)
remove_monitor(MEMORY_CURRENTLY_ALLOCATED_CATEGORY)
remove_monitor(MEMORY_MAX_ALLOCATED_CATEGORY)
remove_monitor(FILE_SAMPLE_CATEGORY)
remove_monitor(FILE_STREAM_CATEGORY)
remove_monitor(FILE_OTHER_CATEGORY)
func add_monitor(title: String, callable: Callable) -> void:
if not Performance.has_custom_monitor(title):
Performance.add_custom_monitor(title, callable)
func remove_monitor(title: String) -> void:
if Performance.has_custom_monitor(title):
Performance.remove_custom_monitor(title)
================================================
FILE: demo/addons/fmod/tool/performances/PerformancesDisplay.gd.uid
================================================
uid://bc0uajlvc0u00
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodBankPathEditorProperty.gd
================================================
class_name FmodBankPathEditorProperty extends EditorProperty
var path_property_name := "bank_paths"
var ui: Control
var last_selected_index := -1
func _init(open_project_explorer_callable: Callable):
ui = load("res://addons/fmod/tool/property_editors/FmodBankPathsPropertyEditorUi.tscn").instantiate()
add_child(ui)
var add_button: Button = ui.get_node("%AddButton")
var open_project_explorer_event = func open_project_explorer_event():
open_project_explorer_callable.call(self._set_path_and_guid)
add_button.pressed.connect(open_project_explorer_event)
var remove_button: Button = ui.get_node("%RemoveButton")
remove_button.pressed.connect(_on_remove_button)
var manual_add_button: Button = ui.get_node("%ManualAddButton")
manual_add_button.pressed.connect(_on_manual_add_button)
var up_button: Button = ui.get_node("%UpButton")
up_button.pressed.connect(_on_move_button.bind(false))
var down_button: Button = ui.get_node("%DownButton")
down_button.pressed.connect(_on_move_button.bind(true))
func _update_property():
var bank_list: ItemList = ui.get_node("%BankList")
bank_list.clear()
var bank_paths: Array = get_edited_object()[path_property_name]
for path in bank_paths:
bank_list.add_item(path)
if last_selected_index == -1:
return
bank_list.select(last_selected_index)
last_selected_index = -1
func _set_path_and_guid(path: String, _cancel: String):
var current_bank_paths: Array = get_edited_object()[path_property_name]
if current_bank_paths.has(path):
return
var bank_paths := Array(current_bank_paths)
bank_paths.append(path)
emit_changed(path_property_name, bank_paths)
func _on_remove_button():
var bank_list: ItemList = ui.get_node("%BankList")
var current_bank_paths: Array = get_edited_object()[path_property_name]
var bank_paths := Array(current_bank_paths)
var selected_items_indexes: PackedInt32Array = bank_list.get_selected_items()
if selected_items_indexes.is_empty():
return
var item = bank_list.get_item_text(selected_items_indexes[0])
var in_list_index = bank_paths.find(item)
bank_paths.remove_at(in_list_index)
last_selected_index = in_list_index if in_list_index < bank_paths.size() else in_list_index - 1
emit_changed(path_property_name, bank_paths)
func _on_manual_add_button():
var manual_add_line_edit: LineEdit = ui.get_node("%ManualAddLineEdit")
var to_add: String = manual_add_line_edit.text
if not to_add.begins_with("res://") || not to_add.ends_with(".bank"):
return
_set_path_and_guid(to_add, "")
manual_add_line_edit.text = ""
func _on_move_button(is_down: bool):
var bank_list: ItemList = ui.get_node("%BankList")
var current_bank_paths: Array = get_edited_object()[path_property_name]
var bank_paths := Array(current_bank_paths)
var selected_items_indexes: PackedInt32Array = bank_list.get_selected_items()
if selected_items_indexes.is_empty():
return
var item = bank_list.get_item_text(selected_items_indexes[0])
var in_list_index = bank_paths.find(item)
var boundary = current_bank_paths.size() - 1 if is_down else 0
if in_list_index == boundary:
return
var new_index = in_list_index + 1 if is_down else in_list_index - 1
bank_paths.remove_at(in_list_index)
bank_paths.insert(new_index, item)
last_selected_index = new_index
emit_changed(path_property_name, bank_paths)
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodBankPathEditorProperty.gd.uid
================================================
uid://cxyd4qioylvgr
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodBankPathsPropertyEditorUi.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://dtlwk8wdeal3h"]
[ext_resource type="Texture2D" uid="uid://o2chsr07oeqs" path="res://addons/fmod/icons/bank_icon.svg" id="1_11c48"]
[node name="FmodBankPathsPropertyEditorUi" type="VBoxContainer"]
offset_right = 92.0
offset_bottom = 43.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="AddButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "+"
icon = ExtResource("1_11c48")
icon_alignment = 2
[node name="RemoveButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "-"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer"]
layout_mode = 2
[node name="UpButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "↑"
[node name="DownButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "↓"
[node name="BankList" type="ItemList" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
auto_height = true
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="ManualAddLineEdit" type="LineEdit" parent="HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="ManualAddButton" type="Button" parent="HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
text = "+"
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodEventEditorProperty.gd
================================================
@tool class_name FmodEventEditorProperty extends FmodPathEditorProperty
static var EVENT_PARAMETER_PREFIX_FOR_PROPERTIES = "fmod_parameters"
var former_event_description: FmodEventDescription
func _update_property():
super()
if get_edited_object().event_name == "":
return
_update_parameters()
var event_description: FmodEventDescription = FmodServer.get_event_from_guid(get_edited_object().event_guid)
if event_description == null:
event_description = FmodServer.get_event(get_edited_object().event_name)
former_event_description = event_description
func _set_path_and_guid(path: String, guid: String):
super(path, guid)
if get_edited_object().event_name == "":
return
_update_parameters()
former_event_description = FmodServer.get_event_from_guid(get_edited_object().event_guid)
func _update_parameters():
var event_description: FmodEventDescription = FmodServer.get_event_from_guid(get_edited_object().event_guid)
if event_description == null:
return
if former_event_description != null and event_description != former_event_description:
get_edited_object().tool_remove_all_parameters()
var map_to_property_name = func map_to_property_name(dict: Dictionary):
return dict["name"]
var filter_fmod_parameter_property = func filter_fmod_parameter_property(parameter_name: String):
return parameter_name.begins_with(EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)
var filter_property_id = func filter_property_id(property: String):
return property.ends_with("/id")
var existing_property_ids = get_edited_object().get_property_list().map(map_to_property_name).filter(filter_fmod_parameter_property).filter(filter_property_id)
var map_property_name_to_parameter_name = func map_property_name_to_parameter_name(parameter: String):
return parameter.split("/")[1]
var existing_parameter_names = existing_property_ids.map(map_property_name_to_parameter_name)
var map_property_id_to_parameter_id_value = func map_property_id_to_parameter_id_value(property: String):
return get_edited_object()[property]
var existing_parameter_ids = existing_property_ids.map(map_property_id_to_parameter_id_value)
var property_matching = existing_parameter_ids.map(func(id): return false)
for param: FmodParameterDescription in event_description.get_parameters():
if param.is_global() or param.is_automatic() or param.is_read_only():
continue
var parameter_name = param.get_name()
var parameter_id_param = "%s/%s/id" % [EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name]
var parameter_value_param = "%s/%s" % [EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name]
var parameter_variant_type = "%s/%s/variant_type" % [EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name]
var parameter_labels = "%s/%s/labels" % [EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name]
var existing_property_name_index = existing_property_ids.find(parameter_id_param)
var are_properties_already_in_node = existing_property_name_index != -1
var parameter_id = param.get_id()
var variant_type: Variant.Type = TYPE_FLOAT
var default_value = param.get_default_value()
var minimum_value = param.get_minimum()
var maximum_value = param.get_maximum()
if param.is_labeled():
variant_type = TYPE_STRING
default_value = event_description.get_parameter_label_by_id(parameter_id, default_value)
minimum_value = event_description.get_parameter_label_by_id(parameter_id, minimum_value)
maximum_value = event_description.get_parameter_label_by_id(parameter_id, maximum_value)
elif param.is_discrete():
variant_type = TYPE_INT
default_value = int(default_value)
minimum_value = int(minimum_value)
maximum_value = int(maximum_value)
if are_properties_already_in_node:
property_matching[existing_property_name_index] = existing_parameter_ids[existing_property_name_index] == parameter_id
if not are_properties_already_in_node or get_edited_object()[parameter_id_param] == null:
get_edited_object()[parameter_id_param] = parameter_id
if not are_properties_already_in_node or get_edited_object()[parameter_value_param] == null:
get_edited_object()[parameter_value_param] = default_value
if not are_properties_already_in_node or get_edited_object()[parameter_variant_type] == null:
get_edited_object()[parameter_variant_type] = variant_type
if param.is_labeled() and (not are_properties_already_in_node or get_edited_object()[parameter_labels] == null):
get_edited_object()[parameter_labels] = event_description.get_parameter_labels_by_id(parameter_id)
for i in property_matching.size():
if not property_matching[i]:
get_edited_object().tool_remove_parameter(existing_parameter_ids[i])
get_edited_object().notify_property_list_changed()
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodEventEditorProperty.gd.uid
================================================
uid://b32x60k0th8td
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodEventEditorProperty.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://cowfthogh1n7i"]
[ext_resource type="PackedScene" uid="uid://cujo3xq0erren" path="res://addons/fmod/tool/property_editors/FmodPathEditorProperty.tscn" id="1_xvpec"]
[ext_resource type="Script" uid="uid://b32x60k0th8td" path="res://addons/fmod/tool/property_editors/FmodEventEditorProperty.gd" id="2_nkhkm"]
[node name="FmodEventEditorProperty" instance=ExtResource("1_xvpec")]
script = ExtResource("2_nkhkm")
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodGuidAndPathPropertyEditorUi.gd
================================================
@tool class_name FmodGuidAndPathPropertyEditorUi extends HBoxContainer
func set_callables(window_callable: Callable, path_callable: Callable, guid_callable: Callable):
%EventExplorerButton.pressed.connect(window_callable)
%PathLineEdit.text_changed.connect(path_callable)
%GuidLineEdit.text_changed.connect(guid_callable)
func set_icon(icon: Texture2D):
%EventExplorerButton.icon = icon
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodGuidAndPathPropertyEditorUi.gd.uid
================================================
uid://3xn18ci172v4
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodGuidAndPathPropertyEditorUi.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://hy04frgfhtgu"]
[ext_resource type="Script" uid="uid://3xn18ci172v4" path="res://addons/fmod/tool/property_editors/FmodGuidAndPathPropertyEditorUi.gd" id="1_eao7t"]
[ext_resource type="Texture2D" uid="uid://cmpgmbn3y4svl" path="res://addons/fmod/icons/event_icon.svg" id="1_kuu6i"]
[node name="FmodGuidAndPathPropertyEditorUi" type="HBoxContainer"]
offset_right = 1152.0
offset_bottom = 66.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_eao7t")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="PathLineEdit" type="LineEdit" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="GuidLineEdit" type="LineEdit" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="EventExplorerButton" type="Button" parent="."]
unique_name_in_owner = true
layout_mode = 2
icon = ExtResource("1_kuu6i")
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodPathEditorProperty.gd
================================================
@tool class_name FmodPathEditorProperty extends EditorProperty
var ui: Control
var guid_property: String
var path_property: String
var regex := RegEx.new()
var default_line_edit_modulate: Color
func initialize(open_project_explorer_callable: Callable, path_prop: String, guid_prop: String):
regex.compile("{\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}}")
guid_property = guid_prop
path_property = path_prop
var guid_and_path_ui: FmodGuidAndPathPropertyEditorUi = %FmodGuidAndPathPropertyEditorUi
default_line_edit_modulate = guid_and_path_ui.get_node("%GuidLineEdit").modulate
var open_project_explorer_event = func open_project_explorer_event():
open_project_explorer_callable.call(self._set_path_and_guid)
guid_and_path_ui.set_callables(open_project_explorer_event, _set_path, _set_guid)
func _update_property():
var guid_and_path_ui = %FmodGuidAndPathPropertyEditorUi
guid_and_path_ui.get_node("%PathLineEdit").text = get_edited_object()[path_property]
guid_and_path_ui.get_node("%GuidLineEdit").text = get_edited_object()[guid_property]
func _set_path(path: String):
emit_changed(path_property, path)
func _set_guid(guid: String):
var line_edit := %FmodGuidAndPathPropertyEditorUi.get_node("%GuidLineEdit") as LineEdit
if not regex.search(guid):
line_edit.modulate = Color.RED
return
line_edit.modulate = default_line_edit_modulate
emit_changed(guid_property, guid)
func _set_path_and_guid(path: String, guid: String):
_set_path(path)
_set_guid(guid)
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodPathEditorProperty.gd.uid
================================================
uid://qshng8csi2fr
================================================
FILE: demo/addons/fmod/tool/property_editors/FmodPathEditorProperty.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://cujo3xq0erren"]
[ext_resource type="Script" uid="uid://qshng8csi2fr" path="res://addons/fmod/tool/property_editors/FmodPathEditorProperty.gd" id="1_4e4vx"]
[ext_resource type="PackedScene" uid="uid://hy04frgfhtgu" path="res://addons/fmod/tool/property_editors/FmodGuidAndPathPropertyEditorUi.tscn" id="2_nvtqg"]
[node name="FmodPathEditorProperty" type="EditorProperty"]
script = ExtResource("1_4e4vx")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="FmodGuidAndPathPropertyEditorUi" parent="VBoxContainer" instance=ExtResource("2_nvtqg")]
unique_name_in_owner = true
layout_mode = 2
================================================
FILE: demo/addons/fmod/tool/ui/EventParametersDisplay.gd
================================================
@tool class_name EventParametersDisplay extends ScrollContainer
static var parameter_display_scene: PackedScene = load("res://addons/fmod/tool/ui/ParameterDisplay.tscn")
func set_fmod_event(event: FmodEventDescription) -> bool: # returns false if there were no parameters
for child in %ParameterDisplaysContainer.get_children():
%ParameterDisplaysContainer.remove_child(child)
child.queue_free()
var event_parameters: Array = event.get_parameters()
if event_parameters:
show()
for parameter : FmodParameterDescription in event_parameters:
var parameter_display: ParameterDisplay = parameter_display_scene.instantiate()
parameter_display.set_event_description(event)
parameter_display.set_parameter(parameter)
if %ParameterDisplaysContainer.get_child_count() > 0:
%ParameterDisplaysContainer.add_child(HSeparator.new())
%ParameterDisplaysContainer.add_child(parameter_display)
return true # we had parameters to show!
else:
return false # no parameters to visualise
================================================
FILE: demo/addons/fmod/tool/ui/EventParametersDisplay.gd.uid
================================================
uid://7relkis52fsu
================================================
FILE: demo/addons/fmod/tool/ui/EventParametersDisplay.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://cppeyr1ke5wre"]
[ext_resource type="Script" uid="uid://7relkis52fsu" path="res://addons/fmod/tool/ui/EventParametersDisplay.gd" id="1_2l58q"]
[node name="EventParametersDisplay" type="ScrollContainer"]
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
script = ExtResource("1_2l58q")
[node name="ParameterDisplaysContainer" type="VBoxContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
================================================
FILE: demo/addons/fmod/tool/ui/EventParametersWindow.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://skp8awewyl85"]
[ext_resource type="PackedScene" uid="uid://cppeyr1ke5wre" path="res://addons/fmod/tool/ui/EventParametersDisplay.tscn" id="1_clkxg"]
[node name="EventParametersWindow" type="Window"]
[node name="EventParametersDisplay" parent="." instance=ExtResource("1_clkxg")]
================================================
FILE: demo/addons/fmod/tool/ui/EventPlayControls.gd
================================================
@tool
extends PanelContainer
@export var play_button : Button
@export var stop_button : Button
@export var fade_out_toggle : CheckButton
var event_instance : FmodEvent
func _ready() -> void:
hide()
play_button.icon = EditorInterface.get_editor_theme().get_icon("Play", "EditorIcons")
stop_button.icon = EditorInterface.get_editor_theme().get_icon("Stop", "EditorIcons")
play_button.pressed.connect(on_play_button_pressed)
stop_button.pressed.connect(on_stop_button_pressed)
func set_fmod_event(_event_description : FmodEventDescription) -> void:
stop_and_release_instance() # always stop and release if a previous one is active
event_instance = FmodServer.create_event_instance_with_guid(_event_description.get_guid())
show()
func play_event() -> void:
if event_instance:
event_instance.start()
func stop_event() -> void:
if event_instance:
var stop_mode : int = FmodServer.FMOD_STUDIO_STOP_IMMEDIATE
if fade_out_toggle.button_pressed:
stop_mode = FmodServer.FMOD_STUDIO_STOP_ALLOWFADEOUT
event_instance.stop(stop_mode)
func _exit_tree() -> void:
stop_and_release_instance()
func stop_and_release_instance() -> void:
if event_instance:
event_instance.stop(0)
event_instance.release()
func on_play_button_pressed() -> void:
play_event()
func on_stop_button_pressed() -> void:
stop_event()
================================================
FILE: demo/addons/fmod/tool/ui/EventPlayControls.gd.uid
================================================
uid://vgmq7hfrbddw
================================================
FILE: demo/addons/fmod/tool/ui/FmodBankExplorer.gd
================================================
@tool class_name FmodBankExplorer extends Window
enum ToDisplayFlags {
BUSES = 1,
VCA = 2,
EVENTS = 4
}
static var _fmod_icon = load("res://addons/fmod/icons/fmod_icon.svg")
static var _vca_icon = load("res://addons/fmod/icons/vca_icon.svg")
static var _bank_icon = load("res://addons/fmod/icons/bank_icon.svg")
static var _event_icon = load("res://addons/fmod/icons/event_icon.svg")
static var _bus_icon = load("res://addons/fmod/icons/bus_icon.svg")
static var _snapshot_icon = load("res://addons/fmod/icons/snapshot_icon.svg")
signal emit_path_and_guid(path: String, guid: String)
var tree: Tree
@onready var copy_path_button := %PathLabel.get_child(0)
@onready var copy_guid_button := %GuidLabel.get_child(0)
var should_display_copy_buttons = true
var should_display_select_button = false
var _current_select_callable: Callable
var base_color: Color
var contrast: float
var background_color: Color
var banks: Array = Array()
var flags: int = 0
var search: String = ""
func reset_search():
%SearchField.text = ""
search = ""
func _ready():
var main_window_size = get_parent().get_window().size
size = main_window_size * 0.5
var copy_texture : Texture = EditorInterface.get_editor_theme().get_icon("ActionCopy", "EditorIcons")
copy_guid_button.icon = copy_texture
copy_path_button.icon = copy_texture
copy_guid_button.visible = false
copy_path_button.visible = false
copy_path_button.pressed.connect(_on_copy_path_button)
copy_guid_button.pressed.connect(_on_copy_guid_button)
var emit_path_and_guid_callable = func emit_path_and_guid_callable():
var selected_item = tree.get_selected()
if selected_item == null:
return
var selected_fmod_element = selected_item.get_metadata(0)
if selected_fmod_element == null:
return
var path = selected_fmod_element.get_godot_res_path() if selected_fmod_element is FmodBank else selected_fmod_element.get_path()
emit_path_and_guid.emit(path, selected_fmod_element.get_guid())
%SelectButton.pressed.connect(emit_path_and_guid_callable)
%SelectButton.pressed.connect(close_window)
%CloseButton.pressed.connect(close_window)
close_requested.connect(close_window)
tree = %Tree
tree.item_selected.connect(_on_item_selected)
tree.columns = 1
generate_tree()
%RefreshBanksButton.pressed.connect(on_refresh_banks_button_pressed)
func regenerate_tree(callable: Callable = Callable()):
tree.clear()
generate_tree(callable)
func generate_tree(callable: Callable = Callable()):
%SelectButton.visible = should_display_select_button
if _current_select_callable != Callable() && _current_select_callable.get_object() != null:
emit_path_and_guid.disconnect(_current_select_callable)
_current_select_callable = callable
var root_item := tree.create_item()
root_item.set_text(0, "Fmod objects")
root_item.set_icon(0, _fmod_icon)
var has_many_flags = (flags & (flags - 1)) != 0
for bank in FmodServer.get_all_banks():
var fmod_bank := bank as FmodBank
var buses := fmod_bank.get_bus_list()
var vcas := fmod_bank.get_vca_list()
var events := fmod_bank.get_description_list()
if search.is_empty():
var bank_item := tree.create_item(root_item)
bank_item.set_text(0, fmod_bank.get_godot_res_path())
bank_item.set_icon(0, _bank_icon)
bank_item.set_metadata(0, bank)
if flags & ToDisplayFlags.BUSES and buses.size() != 0:
buses.sort_custom(sort_by_path)
if has_many_flags:
var buses_item := tree.create_item(bank_item)
buses_item.set_text(0, "Buses")
buses_item.set_icon(0, _bus_icon)
_add_elements_as_tree(buses, buses_item)
else:
_add_elements_as_tree(buses, bank_item)
if flags & ToDisplayFlags.VCA and vcas.size() != 0:
vcas.sort_custom(sort_by_path)
if has_many_flags:
var vca_item := tree.create_item(bank_item)
vca_item.set_text(0, "VCAs")
vca_item.set_icon(0, _vca_icon)
_add_elements_as_tree(vcas, vca_item)
else:
_add_elements_as_tree(vcas, bank_item)
if flags & ToDisplayFlags.EVENTS and events.size() != 0:
events.sort_custom(sort_by_path)
if has_many_flags:
var events_item := tree.create_item(bank_item)
events_item.set_text(0, "Events")
events_item.set_icon(0, _event_icon)
_add_elements_as_tree(events, events_item)
else:
_add_elements_as_tree(events, bank_item)
else:
if flags & ToDisplayFlags.BUSES:
_add_elements_as_items(buses, root_item)
if flags & ToDisplayFlags.VCA:
_add_elements_as_items(vcas, root_item)
if flags & ToDisplayFlags.EVENTS:
_add_elements_as_items(events, root_item)
if copy_path_button.visible:
copy_path_button.visible = should_display_copy_buttons
if copy_guid_button.visible:
copy_guid_button.visible = should_display_copy_buttons
if _current_select_callable != Callable():
print(_current_select_callable)
emit_path_and_guid.connect(_current_select_callable)
%SelectButton.visible = should_display_select_button and %GuidLabel.text != ""
func _add_elements_as_items(elements: Array, parent: TreeItem):
for element in elements:
var full_path: String = element.get_path()
if not full_path.containsn(search):
continue
var child := tree.create_item(parent)
var name := full_path.rsplit("/")[-1]
child.set_text(0, name)
child.set_metadata(0, element)
child.set_icon(0, _get_icon_for_fmod_path(full_path))
func _add_elements_as_tree(elements: Array, parent: TreeItem):
var nodes := { "": parent }
for element in elements:
var full_path: String = element.get_path()
var parts := full_path.split("/")
# Drop the “type:” prefix
if parts.size() > 0:
parts.remove_at(0)
# Walk each segment in turn, building a running “key”
var key := ""
for i in range(parts.size()):
var name = parts[i]
if full_path == "bus:/":
name = "Master"
if key == "":
key = name
else:
key = key + "/" + name
# If we haven’t created this node yet, do so now
if not nodes.has(key):
var root_and_name := key.rsplit("/", false, 1)
var parent_key: String = ""
if root_and_name.size() == 2:
parent_key = root_and_name[0]
var parent_item = nodes[parent_key]
var child := tree.create_item(parent_item)
child.set_text(0, name)
nodes[key] = child
# If this is the final segment, attach the metadata & icon
if i == parts.size() - 1:
var leaf = nodes[key]
leaf.set_metadata(0, element)
leaf.set_icon(0, _get_icon_for_fmod_path(full_path))
func _on_item_selected():
var metadata = tree.get_selected().get_metadata(0)
if metadata == null or !metadata.get_guid():
%PathsBG.hide()
%EventPlayControls.hide()
copy_path_button.visible = false
copy_guid_button.visible = false
%SelectButton.visible = false
%ParametersLabel.visible = false
%ParametersContainer.visible = false
return
%GuidLabel.set_text(metadata.get_guid())
%PathLabel.set_text(metadata.get_path())
%PathsBG.show()
if should_display_copy_buttons:
copy_path_button.visible = true
copy_guid_button.visible = true
if should_display_select_button:
%SelectButton.visible = true
if metadata is FmodEventDescription:
%EventPlayControls.set_fmod_event(metadata)
var _show_parameter_controls : bool = %EventParametersDisplay.set_fmod_event(metadata)
%ParametersLabel.visible = _show_parameter_controls
%ParametersContainer.visible = _show_parameter_controls
return
%EventPlayControls.hide()
%EventParametersDisplay.hide()
%ParametersLabel.visible = false
%ParametersContainer.visible = false
func _on_copy_path_button():
DisplayServer.clipboard_set(%PathLabel.text)
func _on_copy_guid_button():
DisplayServer.clipboard_set(%GuidLabel.text)
func on_refresh_banks_button_pressed() -> void:
# unload banks
banks.clear()
tree.clear()
FmodBankDatabase.reload_all_banks()
generate_tree()
func close_window():
%EventPlayControls.stop_event()
visible = false
static func _get_icon_for_fmod_path(fmod_path: String) -> Texture2D:
var icon: Texture2D = null
if fmod_path.begins_with("bus:/"):
icon = _bus_icon
elif fmod_path.begins_with("event:/"):
icon = _event_icon
elif fmod_path.begins_with("vca:/"):
icon = _vca_icon
elif fmod_path.begins_with("snapshot:/"):
icon = _snapshot_icon
return icon
static func sort_by_path(a, b):
return a.get_path().casecmp_to(b.get_path()) < 0
func _on_text_edit_text_submitted(new_text: String) -> void:
search = new_text
regenerate_tree()
================================================
FILE: demo/addons/fmod/tool/ui/FmodBankExplorer.gd.uid
================================================
uid://b5xgbibc3amtk
================================================
FILE: demo/addons/fmod/tool/ui/FmodBankExplorer.tscn
================================================
[gd_scene load_steps=17 format=3 uid="uid://nr38urn226al"]
[ext_resource type="Script" uid="uid://b5xgbibc3amtk" path="res://addons/fmod/tool/ui/FmodBankExplorer.gd" id="1_ekqus"]
[ext_resource type="Script" uid="uid://vgmq7hfrbddw" path="res://addons/fmod/tool/ui/EventPlayControls.gd" id="2_mleop"]
[ext_resource type="PackedScene" uid="uid://cppeyr1ke5wre" path="res://addons/fmod/tool/ui/EventParametersDisplay.tscn" id="2_uoyg8"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_potss"]
bg_color = Color(0.21176471, 0.23921569, 0.2901961, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_piloo"]
bg_color = Color(0.21176471, 0.23921569, 0.2901961, 1)
[sub_resource type="Theme" id="Theme_02ixt"]
Button/styles/normal = SubResource("StyleBoxFlat_potss")
LineEdit/styles/normal = SubResource("StyleBoxFlat_piloo")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wrr0m"]
bg_color = Color(0, 0, 0, 0.247059)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(1, 1, 1, 0.207843)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2pbsy"]
[sub_resource type="LabelSettings" id="LabelSettings_3jkpq"]
font_size = 18
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0awfk"]
bg_color = Color(0, 0, 0, 0.14902)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.8, 0.8, 0.8, 0.145098)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="LabelSettings" id="LabelSettings_d4isr"]
[sub_resource type="DPITexture" id="DPITexture_potss"]
_source = "
"
base_scale = 1.25
color_map = {
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
}
[sub_resource type="DPITexture" id="DPITexture_piloo"]
_source = "
"
base_scale = 1.25
color_map = {
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
}
[sub_resource type="DPITexture" id="DPITexture_02ixt"]
_source = "
"
base_scale = 1.25
color_map = {
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
Color(1, 0.8666667, 0.39607844, 1): Color(1, 0.87, 0.4, 1)
}
[sub_resource type="InputEventKey" id="InputEventKey_w47tf"]
device = -1
keycode = 4194305
[sub_resource type="Shortcut" id="Shortcut_rarey"]
events = [SubResource("InputEventKey_w47tf")]
[node name="FmodBankExplorer" type="Window"]
oversampling_override = 1.0
title = "Fmod banks explorer"
initial_position = 2
size = Vector2i(1280, 673)
script = ExtResource("1_ekqus")
[node name="BGPanel" type="Panel" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="WindowMargin" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_02ixt")
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 16
[node name="VBoxContainer" type="VBoxContainer" parent="WindowMargin"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="TopPanel" type="PanelContainer" parent="WindowMargin/VBoxContainer"]
custom_minimum_size = Vector2(0, 42.315)
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="WindowMargin/VBoxContainer/TopPanel"]
layout_mode = 2
alignment = 2
[node name="SearchField" type="LineEdit" parent="WindowMargin/VBoxContainer/TopPanel/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
size_flags_horizontal = 2
placeholder_text = "Search.."
context_menu_enabled = false
[node name="RefreshBanksButton" type="Button" parent="WindowMargin/VBoxContainer/TopPanel/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Refresh Banks"
[node name="BaseColorPanel" type="PanelContainer" parent="WindowMargin/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_wrr0m")
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="HSplitContainer" type="HSplitContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer"]
layout_mode = 2
[node name="Tree" type="Tree" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
size_flags_stretch_ratio = 0.54
theme_override_styles/panel = SubResource("StyleBoxEmpty_2pbsy")
hide_root = true
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="RightPanelContent" type="VBoxContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="PathsLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent"]
visible = false
layout_mode = 2
size_flags_vertical = 1
text = "ID:"
label_settings = SubResource("LabelSettings_3jkpq")
justification_flags = 35
[node name="PathsBG" type="PanelContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_0awfk")
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="PathContainer" type="HBoxContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer"]
layout_mode = 2
[node name="TitleLabelContainer" type="VBoxContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer"]
layout_mode = 2
[node name="PathTitleLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/TitleLabelContainer"]
layout_mode = 2
size_flags_vertical = 6
text = "Path:"
label_settings = SubResource("LabelSettings_d4isr")
[node name="GuidTitleLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/TitleLabelContainer"]
layout_mode = 2
size_flags_vertical = 6
text = "GUID: "
label_settings = SubResource("LabelSettings_d4isr")
[node name="ValueContainer" type="VBoxContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PathLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/ValueContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 6
text = "asdfasdfasdfasdf"
[node name="CopyPathLabel" type="Button" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/ValueContainer/PathLabel"]
visible = false
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = 5.57001
offset_top = -15.5
offset_right = 36.57
offset_bottom = 15.5
grow_horizontal = 0
grow_vertical = 2
icon = SubResource("DPITexture_potss")
[node name="GuidLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/ValueContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 6
text = "asdfasdf"
vertical_alignment = 1
[node name="CopyGuidLabel" type="Button" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/PathsBG/MarginContainer/PathContainer/ValueContainer/GuidLabel"]
visible = false
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = 6.095
offset_top = -15.5
offset_right = 37.095
offset_bottom = 15.5
grow_horizontal = 0
grow_vertical = 2
icon = SubResource("DPITexture_potss")
[node name="ParametersLabel" type="Label" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 45)
layout_mode = 2
text = "Parameters:"
label_settings = SubResource("LabelSettings_3jkpq")
vertical_alignment = 2
[node name="ParametersContainer" type="PanelContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_0awfk")
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/ParametersContainer"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="EventParametersDisplay" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/ParametersContainer/MarginContainer" instance=ExtResource("2_uoyg8")]
unique_name_in_owner = true
layout_mode = 2
[node name="EventPlayControls" type="PanelContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent" node_paths=PackedStringArray("play_button", "stop_button", "fade_out_toggle")]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 55.44)
layout_mode = 2
size_flags_vertical = 10
size_flags_stretch_ratio = 0.1
theme_override_styles/panel = SubResource("StyleBoxFlat_0awfk")
script = ExtResource("2_mleop")
play_button = NodePath("MarginContainer/HBoxContainer/PlayEventButton")
stop_button = NodePath("MarginContainer/HBoxContainer/StopEventButton")
fade_out_toggle = NodePath("MarginContainer/HBoxContainer/FadeoutToggle")
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/EventPlayControls"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/EventPlayControls/MarginContainer"]
layout_mode = 2
[node name="PlayEventButton" type="Button" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/EventPlayControls/MarginContainer/HBoxContainer"]
layout_mode = 2
text = "Play"
icon = SubResource("DPITexture_piloo")
[node name="StopEventButton" type="Button" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/EventPlayControls/MarginContainer/HBoxContainer"]
layout_mode = 2
text = "Stop"
icon = SubResource("DPITexture_02ixt")
[node name="FadeoutToggle" type="CheckButton" parent="WindowMargin/VBoxContainer/BaseColorPanel/MarginContainer/HSplitContainer/MarginContainer/RightPanelContent/EventPlayControls/MarginContainer/HBoxContainer"]
layout_mode = 2
text = "Allow fade out"
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_top = 8
[node name="HBoxContainer" type="HBoxContainer" parent="WindowMargin/VBoxContainer/MarginContainer"]
layout_mode = 2
alignment = 1
[node name="MarginContainer2" type="MarginContainer" parent="WindowMargin/VBoxContainer/MarginContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_right = 8
[node name="SelectButton" type="Button" parent="WindowMargin/VBoxContainer/MarginContainer/HBoxContainer/MarginContainer2"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 4
text = "Select"
[node name="MarginContainer" type="MarginContainer" parent="WindowMargin/VBoxContainer/MarginContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_right = 8
[node name="CloseButton" type="Button" parent="WindowMargin/VBoxContainer/MarginContainer/HBoxContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
shortcut = SubResource("Shortcut_rarey")
text = "Close"
[connection signal="text_submitted" from="WindowMargin/VBoxContainer/TopPanel/HBoxContainer/SearchField" to="." method="_on_text_edit_text_submitted"]
================================================
FILE: demo/addons/fmod/tool/ui/ParameterDisplay.gd
================================================
@tool class_name ParameterDisplay extends MarginContainer
var event_description: FmodEventDescription
var parameter: FmodParameterDescription
func set_event_description(p_event_description: FmodEventDescription):
event_description = p_event_description
func set_parameter(p_parameter: FmodParameterDescription):
show()
parameter = p_parameter
func display_value_selector(should: bool):
%ValueSetterContainer.visible = should
func _ready():
if parameter == null:
hide()
return
var minimum_value = parameter.get_minimum()
var maximum_value = parameter.get_maximum()
var default_value = parameter.get_default_value()
var copy_icon : Texture = EditorInterface.get_editor_theme().get_icon("ActionCopy", "EditorIcons")
%NameCopyButton.icon = copy_icon
%IdCopyButton.icon = copy_icon
%NameLabel.text = parameter.get_name()
%IdLabel.text = str(parameter.get_id())
if parameter.is_labeled():
%RangeTitle.text = "Values"
var values_text = "["
var is_first: bool = true
for label: String in event_description.get_parameter_labels_by_id(parameter.get_id()):
if not is_first:
values_text += ", "
values_text += label
is_first = false
values_text += "]"
%RangeLabel.text = values_text
else:
%RangeLabel.text = "[%s, %s]" % [minimum_value, maximum_value]
%DefaultValueLabel.text = str(default_value)
%NameCopyButton.pressed.connect(_on_copy_name_button)
%IdCopyButton.pressed.connect(_on_copy_id_button)
%BackToDefaultButton.pressed.connect(_on_default_value_button)
%ValueSlider.min_value = minimum_value
%ValueSlider.max_value = maximum_value
%ValueSlider.value = default_value
_on_slider_value_changed(%ValueSlider.value)
%ValueSlider.value_changed.connect(_on_slider_value_changed)
func _on_copy_name_button():
DisplayServer.clipboard_set(%NameLabel.text)
func _on_copy_id_button():
DisplayServer.clipboard_set(%IdLabel.text)
func _on_default_value_button():
%ValueSlider.value = parameter.get_default_value()
func _on_slider_value_changed(value: float):
%CurrentValueLabel.text = str(value)
================================================
FILE: demo/addons/fmod/tool/ui/ParameterDisplay.gd.uid
================================================
uid://ve6g43nb1hdd
================================================
FILE: demo/addons/fmod/tool/ui/ParameterDisplay.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://bfdldojk5i6u3"]
[ext_resource type="Script" uid="uid://ve6g43nb1hdd" path="res://addons/fmod/tool/ui/ParameterDisplay.gd" id="1_fxyw8"]
[node name="ParameterDisplay" type="MarginContainer"]
visible = false
offset_right = 168.0
offset_bottom = 160.0
size_flags_horizontal = 3
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
script = ExtResource("1_fxyw8")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="VBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="TitleContainer" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="NameTitle" type="Label" parent="VBoxContainer/VBoxContainer/TitleContainer"]
layout_mode = 2
size_flags_vertical = 10
text = "Name: "
[node name="IdTitle" type="Label" parent="VBoxContainer/VBoxContainer/TitleContainer"]
layout_mode = 2
size_flags_vertical = 10
text = "ID: "
[node name="RangeTitle" type="Label" parent="VBoxContainer/VBoxContainer/TitleContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 10
text = "Range: "
[node name="DefaultValueTitle" type="Label" parent="VBoxContainer/VBoxContainer/TitleContainer"]
layout_mode = 2
size_flags_vertical = 10
text = "Default value: "
[node name="ContentContainer" type="VBoxContainer" parent="VBoxContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="NameLabel" type="Label" parent="VBoxContainer/VBoxContainer/ContentContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 10
[node name="NameCopyButton" type="Button" parent="VBoxContainer/VBoxContainer/ContentContainer/NameLabel"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = 9.0
offset_top = -15.5
offset_right = 40.0
offset_bottom = 15.5
grow_horizontal = 0
grow_vertical = 2
size_flags_vertical = 10
[node name="IdLabel" type="Label" parent="VBoxContainer/VBoxContainer/ContentContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 10
[node name="IdCopyButton" type="Button" parent="VBoxContainer/VBoxContainer/ContentContainer/IdLabel"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = 9.0
offset_top = -15.5
offset_right = 40.0
offset_bottom = 15.5
grow_horizontal = 0
grow_vertical = 2
size_flags_vertical = 10
[node name="RangeLabel" type="Label" parent="VBoxContainer/VBoxContainer/ContentContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 10
[node name="DefaultValueLabel" type="Label" parent="VBoxContainer/VBoxContainer/ContentContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 10
[node name="ValueSetterContainer" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/ValueSetterContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ValueSetterContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/ValueSetterContainer/HBoxContainer"]
layout_mode = 2
text = "Set value: "
[node name="ValueSlider" type="HSlider" parent="VBoxContainer/ValueSetterContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
[node name="BackToDefaultButton" type="Button" parent="VBoxContainer/ValueSetterContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Default"
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/ValueSetterContainer"]
layout_mode = 2
[node name="CurrentValueTitleLabel" type="Label" parent="VBoxContainer/ValueSetterContainer/HBoxContainer2"]
layout_mode = 2
text = "Current value: "
[node name="CurrentValueLabel" type="Label" parent="VBoxContainer/ValueSetterContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
[node name="Button" type="Button" parent="VBoxContainer/ValueSetterContainer"]
layout_mode = 2
text = "Select"
================================================
FILE: demo/addons/fmod/tool/ui/TestFmodBankExplorer.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://f4i35731qm63"]
[ext_resource type="PackedScene" uid="uid://nr38urn226al" path="res://addons/fmod/tool/ui/FmodBankExplorer.tscn" id="1_0ul6h"]
[node name="TestFmodBankExplorer" type="Node2D"]
[node name="FmodBankLoader" type="FmodBankLoader" parent="."]
bank_paths = ["res://assets/Banks/Master.strings.bank", "res://assets/Banks/Master.bank", "res://assets/Banks/Music.bank", "res://assets/Banks/Vehicles.bank"]
[node name="FmodBankExplorer" parent="FmodBankLoader" instance=ExtResource("1_0ul6h")]
initial_position = 0
position = Vector2i(0, 36)
================================================
FILE: demo/addons/gut/GutScene.gd
================================================
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)
================================================
FILE: demo/addons/gut/GutScene.gd.uid
================================================
uid://bw7tukh738kw1
================================================
FILE: demo/addons/gut/GutScene.tscn
================================================
[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
================================================
FILE: demo/addons/gut/LICENSE.md
================================================
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.
================================================
FILE: demo/addons/gut/UserFileViewer.gd
================================================
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()
================================================
FILE: demo/addons/gut/UserFileViewer.gd.uid
================================================
uid://x51wilphva3d
================================================
FILE: demo/addons/gut/UserFileViewer.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/autofree.gd
================================================
# ##############################################################################
#(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)
================================================
FILE: demo/addons/gut/autofree.gd.uid
================================================
uid://bxjfriqxgwe0r
================================================
FILE: demo/addons/gut/awaiter.gd
================================================
extends Node
class AwaitLogger:
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 = AwaitLogger.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 || \
_wait_physics_frames != 0 || \
_wait_process_frames != 0
================================================
FILE: demo/addons/gut/awaiter.gd.uid
================================================
uid://ccu4ww35edtdi
================================================
FILE: demo/addons/gut/cli/change_project_warnings.gd
================================================
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()
================================================
FILE: demo/addons/gut/cli/change_project_warnings.gd.uid
================================================
uid://1pauyfnd1cre
================================================
FILE: demo/addons/gut/cli/gut_cli.gd
================================================
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 OptionResolver:
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.')
# 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()
# parse options and run Gut
func main():
var opt_resolver = OptionResolver.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)
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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/cli/gut_cli.gd.uid
================================================
uid://bhuudqinp4bth
================================================
FILE: demo/addons/gut/cli/optparse.gd
================================================
## 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 Option:
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 OptionHeading:
var options = []
var display = 'default'
#-------------------------------------------------------------------------------
# Organizes options by order, heading, position. Also responsible for all
# help related text generation.
#-------------------------------------------------------------------------------
class Options:
var options = []
var positional = []
var default_heading = OptionHeading.new()
var script_option = Option.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 = OptionHeading.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 := Options.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) -> Option:
var op_name: String
var aliases: Array[String] = []
var new_op: Option = 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 = Option.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) -> Option:
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) -> Option:
var new_op = null
if(options.get_by_name(op_name) != null):
push_error(str('Positional option [', op_name, '] already exists.'))
else:
new_op = Option.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) -> Option:
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: Option = 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: Option = 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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/cli/optparse.gd.uid
================================================
uid://c8m4fojwln6bq
================================================
FILE: demo/addons/gut/collected_script.gd
================================================
# ------------------------------------------------------------------------------
# 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
================================================
FILE: demo/addons/gut/collected_script.gd.uid
================================================
uid://bjjcnr1oqvag6
================================================
FILE: demo/addons/gut/collected_test.gd
================================================
# ------------------------------------------------------------------------------
# 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
================================================
FILE: demo/addons/gut/collected_test.gd.uid
================================================
uid://cl854f1m26a2a
================================================
FILE: demo/addons/gut/comparator.gd
================================================
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 '!='
================================================
FILE: demo/addons/gut/comparator.gd.uid
================================================
uid://bohry7fhscy7y
================================================
FILE: demo/addons/gut/compare_result.gd
================================================
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
================================================
FILE: demo/addons/gut/compare_result.gd.uid
================================================
uid://cow1xqmqqvn4e
================================================
FILE: demo/addons/gut/diff_formatter.gd
================================================
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
================================================
FILE: demo/addons/gut/diff_formatter.gd.uid
================================================
uid://ch2km05phxacd
================================================
FILE: demo/addons/gut/diff_tool.gd
================================================
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
================================================
FILE: demo/addons/gut/diff_tool.gd.uid
================================================
uid://beoxokvl1hjs8
================================================
FILE: demo/addons/gut/double_templates/function_template.txt
================================================
{func_decleration}
if(__gutdbl == null):
return
__gutdbl.spy_on('{method_name}', {param_array})
if(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})):
return {super_call}
else:
return await __gutdbl.handle_other_stubs('{method_name}', {param_array})
================================================
FILE: demo/addons/gut/double_templates/init_template.txt
================================================
{func_decleration}:
super({super_params})
__gutdbl.spy_on('{method_name}', {param_array})
================================================
FILE: demo/addons/gut/double_templates/script_template.txt
================================================
# ##############################################################################
# Gut Doubled Script
# ##############################################################################
{extends}
{constants}
{properties}
# ------------------------------------------------------------------------------
# GUT stuff
# ------------------------------------------------------------------------------
var __gutdbl_values = {
thepath = '{path}',
subpath = '{subpath}',
stubber = {stubber_id},
spy = {spy_id},
gut = {gut_id},
from_singleton = '{singleton_name}',
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()
# ------------------------------------------------------------------------------
# Doubled Methods
# ------------------------------------------------------------------------------
================================================
FILE: demo/addons/gut/double_tools.gd
================================================
var thepath = ''
var subpath = ''
var from_singleton = 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
const NO_DEFAULT_VALUE = '!__gut__no__default__value__!'
func _init(double = null):
if(double != null):
var 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)
from_singleton = values.from_singleton
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:
return stubber_ref.get_ref().get_default_value(double_ref.get_ref(), method_name, p_index)
================================================
FILE: demo/addons/gut/double_tools.gd.uid
================================================
uid://tr4khoco1hef
================================================
FILE: demo/addons/gut/doubler.gd
================================================
extends RefCounted
var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
var _script_collector = GutUtils.ScriptCollector.new()
# used by tests for debugging purposes.
var print_source = false
var inner_class_registry = GutUtils.InnerClassRegistry.new()
# ###############
# 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 _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 = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
params.to_call_super()
_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 values = {
# Top sections
"extends":extends_text,
"constants":'',#obj_info.get_constants_text(),
"properties":'',#obj_info.get_properties_text(),
# metadata values
"path":path,
"subpath":GutUtils.nvl(parsed.subpath, ''),
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":'',#GutUtils.nvl(obj_info.get_singleton_name(), ''),
"is_partial":partial,
"doubled_methods":included_methods,
}
return _base_script_text.format(values)
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 += _get_func_text(method.meta)
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 += _get_func_text(method.meta)
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(DblClass, parsed, strategy)
if(print_source):
_lgr.log(str(" path | ", DblClass.resource_path, "\n"))
return DblClass
func _stub_method_default_values(which, parsed, strategy):
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.script_path, 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 _get_func_text(method_hash):
return _method_maker.get_function_text(method_hash) + "\n"
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 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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/doubler.gd.uid
================================================
uid://cpy013l0wqwmg
================================================
FILE: demo/addons/gut/dynamic_gdscript.gd
================================================
@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
================================================
FILE: demo/addons/gut/dynamic_gdscript.gd.uid
================================================
uid://cnbjsrik0p5uf
================================================
FILE: demo/addons/gut/editor_caret_context_notifier.gd
================================================
@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 if you select something
# other than a script from the tree. So if you click a help file and then
# back to the same file, then this will fire for the same script
#
# This can fire multiple times for the same script when a script is opened.
func _on_editor_script_changed(script):
_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)
================================================
FILE: demo/addons/gut/editor_caret_context_notifier.gd.uid
================================================
uid://c110s7a32x4su
================================================
FILE: demo/addons/gut/error_tracker.gd
================================================
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
# ----------------
#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)
_mutex.unlock()
return err
================================================
FILE: demo/addons/gut/error_tracker.gd.uid
================================================
uid://35kxgqotjmlu
================================================
FILE: demo/addons/gut/fonts/OFL.txt
================================================
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.
================================================
FILE: demo/addons/gut/gui/EditorRadioButton.tres
================================================
[gd_resource type="Theme" load_steps=3 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")
================================================
FILE: demo/addons/gut/gui/GutBottomPanel.gd
================================================
@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,
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(_panel_button != null):
_panel_button.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()
# ---------------
# 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():
if(owner is Window):
owner.grab_focus()
else:
_gut_plugin.make_bottom_panel_item_visible(self)
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
================================================
FILE: demo/addons/gut/gui/GutBottomPanel.gd.uid
================================================
uid://dtvnb0xatk0my
================================================
FILE: demo/addons/gut/gui/GutBottomPanel.tscn
================================================
[gd_scene load_steps=10 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="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[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"]
[node name="GutBottomPanel" type="Control"]
custom_minimum_size = Vector2(250, 250)
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="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ControlBar" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="RunAll" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
shortcut = SubResource("9")
text = "Run All"
icon = ExtResource("4")
[node name="Sep3" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
custom_minimum_size = Vector2(648, 0)
layout_mode = 2
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="MakeFloating" type="Button" parent="layout/ControlBar"]
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="ControlBar2" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="Sep2" type="ColorRect" parent="layout/ControlBar2"]
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_name_in_owner = true
custom_minimum_size = Vector2(30, 30)
layout_mode = 2
[node name="Passing" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Passing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Passing"]
layout_mode = 2
text = "Pass"
[node name="passing_value" type="Label" parent="layout/ControlBar2/Passing"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Failing" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Failing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Failing"]
layout_mode = 2
text = "Fail"
[node name="failing_value" type="Label" parent="layout/ControlBar2/Failing"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Pending" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Pending"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Pending"]
layout_mode = 2
text = "Risky"
[node name="pending_value" type="Label" parent="layout/ControlBar2/Pending"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Orphans" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Orphans"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Orphans"]
layout_mode = 2
text = "Orphans"
[node name="orphans_value" type="Label" parent="layout/ControlBar2/Orphans"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Errors" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Errors"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Errors"]
layout_mode = 2
text = "Errors"
[node name="errors_value" type="Label" parent="layout/ControlBar2/Errors"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="Warnings" type="HBoxContainer" parent="layout/ControlBar2"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Warnings"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/ControlBar2/Warnings"]
layout_mode = 2
text = "Warnings"
[node name="warnings_value" type="Label" parent="layout/ControlBar2/Warnings"]
unique_name_in_owner = true
layout_mode = 2
text = "---"
[node name="CenterContainer" type="CenterContainer" parent="layout/ControlBar2"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ExtraButtons" type="HBoxContainer" parent="layout/ControlBar2"]
unique_name_in_owner = true
layout_mode = 2
[node name="Sep1" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunMode" type="Button" parent="layout/ControlBar2/ExtraButtons"]
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"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar2/ExtraButtons"]
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"]
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"]
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"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Shortcuts" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
size_flags_vertical = 11
tooltip_text = "GUT Shortcuts"
icon = ExtResource("4_xv2r3")
[node name="About" type="Button" parent="layout/ControlBar2/ExtraButtons"]
layout_mode = 2
tooltip_text = "About"
text = "A"
[node name="RSplit" type="HSplitContainer" parent="layout"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HSplitResults" type="HSplitContainer" parent="layout/RSplit/CResults"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RunResults" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("5")]
unique_name_in_owner = true
custom_minimum_size = Vector2(255, 255)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputText" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("6")]
unique_name_in_owner = true
layout_mode = 2
[node name="VSplitResults" type="VSplitContainer" parent="layout/RSplit/CResults"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
custom_minimum_size = Vector2(500, 2.08165e-12)
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShortcutDialog" parent="." instance=ExtResource("7_srqj5")]
visible = false
[node name="ShellOutOptions" parent="." instance=ExtResource("7_xv2r3")]
size = Vector2i(1300, 1336)
visible = false
[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="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"]
================================================
FILE: demo/addons/gut/gui/GutControl.gd
================================================
@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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gui/GutControl.gd.uid
================================================
uid://cqlvpwidawld6
================================================
FILE: demo/addons/gut/gui/GutControl.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/GutEditorWindow.gd
================================================
@tool
extends Window
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
@onready var _chk_always_on_top = $Layout/WinControls/OnTop
var _bottom_panel = null
var _ready_to_go = false
var _gut_shortcuts = []
var gut_plugin = null
var interface = null
func _unhandled_key_input(event: InputEvent) -> void:
if(event is InputEventKey):
if(_gut_shortcuts.has(event.as_text_keycode())):
get_tree().root.push_input(event)
func _ready() -> void:
var pref_size = GutEditorGlobals.user_prefs.gut_window_size.value
if(pref_size.x < 0):
size = Vector2(800, 800)
else:
size = pref_size
always_on_top = GutEditorGlobals.user_prefs.gut_window_on_top.value
_chk_always_on_top.button_pressed = always_on_top
# --------
# Events
# --------
func _on_on_top_toggled(toggled_on: bool) -> void:
always_on_top = toggled_on
GutEditorGlobals.user_prefs.gut_window_on_top.value = toggled_on
func _on_size_changed() -> void:
if(_ready_to_go):
GutEditorGlobals.user_prefs.gut_window_size.value = size
func _on_close_requested() -> void:
gut_plugin.toggle_windowed()
func _on_vert_layout_pressed() -> void:
_bottom_panel.results_vert_layout()
func _on_horiz_layout_pressed() -> void:
_bottom_panel.results_horiz_layout()
# --------
# Public
# --------
func add_gut_panel(panel : Control):
$Layout.add_child(panel)
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel.visible = true
_bottom_panel = panel
_ready_to_go = true
panel.owner = self
# This stunk to figure out.
theme = interface.get_editor_theme()
var settings = interface.get_editor_settings()
$ColorRect.color = settings.get_setting("interface/theme/base_color")
set_gut_shortcuts(_bottom_panel._ctrls.shortcut_dialog)
func remove_panel():
$Layout.remove_child(_bottom_panel)
_bottom_panel.owner = null
func set_gut_shortcuts(shortcuts_dialog):
_gut_shortcuts.clear()
for btn in shortcuts_dialog.all_buttons:
_gut_shortcuts.append(btn.get_input_event().as_text_keycode())
================================================
FILE: demo/addons/gut/gui/GutEditorWindow.gd.uid
================================================
uid://crp2af6k4nmf5
================================================
FILE: demo/addons/gut/gui/GutEditorWindow.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://dnnvwlplf1km7"]
[ext_resource type="Script" uid="uid://crp2af6k4nmf5" path="res://addons/gut/gui/GutEditorWindow.gd" id="1_qevl3"]
[ext_resource type="Texture2D" uid="uid://bdyiwc34kad0n" path="res://addons/gut/images/HSplitContainer.svg" id="2_xw0o2"]
[ext_resource type="Texture2D" uid="uid://cu7rctvlo721o" path="res://addons/gut/images/VSplitContainer.svg" id="3_fqfwy"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qevl3"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0.115499996, 0.132, 0.15949999, 1)
corner_detail = 1
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_af010"]
content_margin_left = 8.0
content_margin_top = 12.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0.21, 0.24, 0.29, 1)
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xw0o2"]
content_margin_left = 0.0
content_margin_top = 8.0
content_margin_right = 0.0
content_margin_bottom = 0.0
bg_color = Color(0.21, 0.24, 0.29, 1)
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fqfwy"]
content_margin_left = 12.0
content_margin_top = 8.0
content_margin_right = 12.0
content_margin_bottom = 8.0
bg_color = Color(0.14699998, 0.16799998, 0.203, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
corner_detail = 5
anti_aliasing = false
[sub_resource type="Theme" id="Theme_fqfwy"]
Editor/colors/accent_color = Color(0.44, 0.73, 0.98, 1)
Editor/colors/background = Color(0.115499996, 0.132, 0.15949999, 1)
Editor/colors/base_color = Color(0.21, 0.24, 0.29, 1)
EditorStyles/styles/Background = SubResource("StyleBoxFlat_qevl3")
EditorStyles/styles/BottomPanel = SubResource("StyleBoxFlat_af010")
EditorStyles/styles/Content = SubResource("StyleBoxFlat_xw0o2")
Panel/styles/panel = SubResource("StyleBoxFlat_fqfwy")
[sub_resource type="ButtonGroup" id="ButtonGroup_qevl3"]
[node name="GutEditorWindow" type="Window"]
oversampling_override = 1.0
title = "GUT"
position = Vector2i(0, 36)
size = Vector2i(800, 800)
min_size = Vector2i(800, 600)
script = ExtResource("1_qevl3")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_fqfwy")
color = Color(0.18717614, 0.18717614, 0.18717614, 1)
[node name="Layout" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="WinControls" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="MenuBar" type="MenuBar" parent="Layout/WinControls"]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_horizontal = 3
flat = true
prefer_global_menu = false
[node name="CenterContainer" type="CenterContainer" parent="Layout/WinControls"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OnTop" type="CheckButton" parent="Layout/WinControls"]
layout_mode = 2
text = "Always on Top"
[node name="HorizLayout" type="Button" parent="Layout/WinControls"]
texture_filter = 1
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_qevl3")
icon = ExtResource("2_xw0o2")
icon_alignment = 1
[node name="VertLayout" type="Button" parent="Layout/WinControls"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_qevl3")
icon = ExtResource("3_fqfwy")
[connection signal="close_requested" from="." to="." method="_on_close_requested"]
[connection signal="size_changed" from="." to="." method="_on_size_changed"]
[connection signal="toggled" from="Layout/WinControls/OnTop" to="." method="_on_on_top_toggled"]
[connection signal="pressed" from="Layout/WinControls/HorizLayout" to="." method="_on_horiz_layout_pressed"]
[connection signal="pressed" from="Layout/WinControls/VertLayout" to="." method="_on_vert_layout_pressed"]
================================================
FILE: demo/addons/gut/gui/GutLogo.tscn
================================================
[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://ckonsqa7kg74h" path="res://addons/gut/images/GutIconV2_base.png" id="2_ba6lh"]
[ext_resource type="Texture2D" uid="uid://beltjeifto36x" 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"]
================================================
FILE: demo/addons/gut/gui/GutRunner.gd
================================================
# ##############################################################################
# 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)
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)
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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gui/GutRunner.gd.uid
================================================
uid://eg8k46gd42a4
================================================
FILE: demo/addons/gut/gui/GutRunner.tscn
================================================
[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")]
================================================
FILE: demo/addons/gut/gui/GutSceneTheme.tres
================================================
[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")
================================================
FILE: demo/addons/gut/gui/MinGui.tscn
================================================
[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
================================================
FILE: demo/addons/gut/gui/NormalGui.tscn
================================================
[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
================================================
FILE: demo/addons/gut/gui/OutputText.gd
================================================
@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.SelectControl.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.NumberControl.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)
================================================
FILE: demo/addons/gut/gui/OutputText.gd.uid
================================================
uid://cax5phqs8acmu
================================================
FILE: demo/addons/gut/gui/OutputText.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/ResizeHandle.gd
================================================
@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()
================================================
FILE: demo/addons/gut/gui/ResizeHandle.gd.uid
================================================
uid://duf6rfdqr6yoc
================================================
FILE: demo/addons/gut/gui/ResizeHandle.tscn
================================================
[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")
================================================
FILE: demo/addons/gut/gui/ResultsTree.gd
================================================
@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)
================================================
FILE: demo/addons/gut/gui/ResultsTree.gd.uid
================================================
uid://dehdhn78qv5tr
================================================
FILE: demo/addons/gut/gui/ResultsTree.tscn
================================================
[gd_scene load_steps=2 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"]
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="."]
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="."]
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
================================================
FILE: demo/addons/gut/gui/RunAtCursor.gd
================================================
@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
================================================
FILE: demo/addons/gut/gui/RunAtCursor.gd.uid
================================================
uid://c4gmgdl1xwflw
================================================
FILE: demo/addons/gut/gui/RunAtCursor.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/RunExternally.gd
================================================
@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
================================================
FILE: demo/addons/gut/gui/RunExternally.gd.uid
================================================
uid://bi8pg352un4om
================================================
FILE: demo/addons/gut/gui/RunExternally.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/RunResults.gd
================================================
@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)
================================================
FILE: demo/addons/gut/gui/RunResults.gd.uid
================================================
uid://chnko3073tkcv
================================================
FILE: demo/addons/gut/gui/RunResults.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/Settings.tscn
================================================
[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
================================================
FILE: demo/addons/gut/gui/ShellOutOptions.gd
================================================
@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 ''
================================================
FILE: demo/addons/gut/gui/ShellOutOptions.gd.uid
================================================
uid://c64u22kybimgi
================================================
FILE: demo/addons/gut/gui/ShellOutOptions.tscn
================================================
[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"
================================================
FILE: demo/addons/gut/gui/ShortcutButton.gd
================================================
@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 || 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()
================================================
FILE: demo/addons/gut/gui/ShortcutButton.gd.uid
================================================
uid://k6hvvpekp0xw
================================================
FILE: demo/addons/gut/gui/ShortcutButton.tscn
================================================
[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"]
================================================
FILE: demo/addons/gut/gui/ShortcutDialog.gd
================================================
@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))
================================================
FILE: demo/addons/gut/gui/ShortcutDialog.gd.uid
================================================
uid://dc5jgemxslgvl
================================================
FILE: demo/addons/gut/gui/ShortcutDialog.tscn
================================================
[gd_scene load_steps=3 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"]
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="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 1912.0
offset_bottom = 1679.0
[node name="Layout" type="VBoxContainer" parent="Scroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShortcutDescription" type="RichTextLabel" parent="Scroll/Layout"]
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"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="CPanelButton" type="HBoxContainer" parent="Scroll/Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CPanelButton"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Show/Hide GUT"
[node name="ShortcutButton" parent="Scroll/Layout/CPanelButton" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription2" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunAll"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run All"
[node name="ShortcutButton" parent="Scroll/Layout/CRunAll" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription3" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentScript"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Script"
[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentScript" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription4" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentInner"]
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" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription5" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunCurrentTest"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Test"
[node name="ShortcutButton" parent="Scroll/Layout/CRunCurrentTest" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription6" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRunAtCursor"]
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" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription7" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CRerun"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Rerun (menu only)"
[node name="ShortcutButton" parent="Scroll/Layout/CRerun" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription8" type="RichTextLabel" parent="Scroll/Layout"]
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"]
layout_mode = 2
[node name="Label" type="Label" parent="Scroll/Layout/CToggleWindowed"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Toggle Windowed"
[node name="ShortcutButton" parent="Scroll/Layout/CToggleWindowed" instance=ExtResource("2_i3wie")]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShortcutDescription9" type="RichTextLabel" parent="Scroll/Layout"]
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
================================================
FILE: demo/addons/gut/gui/about.gd
================================================
@tool
extends AcceptDialog
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var _bbcode = \
"""
[center]GUT {gut_version}[/center]
[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
func _ready():
if(get_parent() is SubViewport):
return
_vert_center_logo()
$Logo.disabled = true
$HBox/Scroll/RichTextLabel.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 _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]')),
"gut_version":GutUtils.version_numbers.gut_version,
})
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
================================================
FILE: demo/addons/gut/gui/about.gd.uid
================================================
uid://g7qu8ihdt3pd
================================================
FILE: demo/addons/gut/gui/about.tscn
================================================
[gd_scene load_steps=5 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://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"]
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="."]
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"]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="Scroll" type="ScrollContainer" parent="HBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RichTextLabel" type="RichTextLabel" parent="HBox/Scroll"]
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="." 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="meta_clicked" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_clicked"]
[connection signal="meta_hover_ended" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_ended"]
[connection signal="meta_hover_started" from="HBox/Scroll/RichTextLabel" to="." method="_on_rich_text_label_meta_hover_started"]
[connection signal="pressed" from="Logo" to="." method="_on_logo_pressed"]
================================================
FILE: demo/addons/gut/gui/editor_globals.gd
================================================
@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
================================================
FILE: demo/addons/gut/gui/editor_globals.gd.uid
================================================
uid://cbi00ubn046c2
================================================
FILE: demo/addons/gut/gui/gut_config_gui.gd
================================================
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.SaveLoadControl.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)
================================================
FILE: demo/addons/gut/gui/gut_config_gui.gd.uid
================================================
uid://chosc1tvfaduq
================================================
FILE: demo/addons/gut/gui/gut_gui.gd
================================================
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
================================================
FILE: demo/addons/gut/gui/gut_gui.gd.uid
================================================
uid://blvhsbnsvfyow
================================================
FILE: demo/addons/gut/gui/gut_logo.gd
================================================
@tool
extends Node2D
class Eyeball:
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 : Eyeball = Eyeball.new($BaseLogo/LeftEye)
@onready var right_eye : Eyeball = Eyeball.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)
================================================
FILE: demo/addons/gut/gui/gut_logo.gd.uid
================================================
uid://b8lvgepb64m8t
================================================
FILE: demo/addons/gut/gui/gut_user_preferences.gd
================================================
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()
================================================
FILE: demo/addons/gut/gui/gut_user_preferences.gd.uid
================================================
uid://dsndkn6whyiov
================================================
FILE: demo/addons/gut/gui/option_maker.gd
================================================
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.NumberControl.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.FloatControl.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.SelectControl.new(disp_text, value, values, hint)
add_ctrl(key, ctrl)
return ctrl
func add_value(key, value, disp_text, hint=''):
var ctrl = PanelControls.StringControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_multiline_text(key, value, disp_text, hint=''):
var ctrl = PanelControls.MultiLineStringControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_boolean(key, value, disp_text, hint=''):
var ctrl = PanelControls.BooleanControl.new(disp_text, value, hint)
add_ctrl(key, ctrl)
return ctrl
func add_directory(key, value, disp_text, hint=''):
var ctrl = PanelControls.DirectoryControl.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.DirectoryControl.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.DirectoryControl.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.ColorControl.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()
================================================
FILE: demo/addons/gut/gui/option_maker.gd.uid
================================================
uid://bjahqsqo645sf
================================================
FILE: demo/addons/gut/gui/panel_controls.gd
================================================
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
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 NumberControl:
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 FloatControl:
extends NumberControl
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 StringControl:
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 MultiLineStringControl:
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 BooleanControl:
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 SelectControl:
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 ColorControl:
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 DirectoryControl:
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 FileDialogSuperPlus:
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 SaveLoadControl:
extends BaseGutPanelControl
var btn_load = Button.new()
var btn_save = Button.new()
var dlg_load := FileDialogSuperPlus.new()
var dlg_save := FileDialogSuperPlus.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
================================================
FILE: demo/addons/gut/gui/panel_controls.gd.uid
================================================
uid://db54jy04d8w7p
================================================
FILE: demo/addons/gut/gui/run_from_editor.gd
================================================
# ------------------------------------------------------------------------------
# This is the entry point when running tests from the editor.
#
# This script should conform to, or ignore, the strictest warning settings.
# ------------------------------------------------------------------------------
extends Node2D
var GutLoader : Object
func _init() -> void:
GutLoader = load("res://addons/gut/gut_loader.gd")
@warning_ignore("unsafe_method_access")
func _ready() -> void:
var runner : Node = load("res://addons/gut/gui/GutRunner.tscn").instantiate()
add_child(runner)
runner.run_from_editor()
GutLoader.restore_ignore_addons()
================================================
FILE: demo/addons/gut/gui/run_from_editor.gd.uid
================================================
uid://bwf2iuidqfkpl
================================================
FILE: demo/addons/gut/gui/run_from_editor.tscn
================================================
[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")
================================================
FILE: demo/addons/gut/gut.gd
================================================
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 are
## 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
signal start_script(test_script_obj)
signal end_script
signal start_test(test_name)
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
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
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)
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 sname = get_current_test_object().collected_script.get_ref().get_filename_and_inner()
var tname = get_current_test_object().name
_orphan_counter.record_orphans(sname, tname)
return _orphan_counter.get_orphan_ids(sname, tname)
# ------------------------------------------------------------------------------
# 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
_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
_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(.5).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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gut.gd.uid
================================================
uid://duvvxvj18c04d
================================================
FILE: demo/addons/gut/gut_cmdln.gd
================================================
# ------------------------------------------------------------------------------
# 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
var Loader : 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)
Loader.restore_ignore_addons()
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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gut_cmdln.gd.uid
================================================
uid://bxw30gwbwnh55
================================================
FILE: demo/addons/gut/gut_config.gd
================================================
# ##############################################################################
#
# 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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gut_config.gd.uid
================================================
uid://bobi58361x1ya
================================================
FILE: demo/addons/gut/gut_fonts.gd
================================================
# ------------------------------------------------------------------------------
# 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])
================================================
FILE: demo/addons/gut/gut_fonts.gd.uid
================================================
uid://dvajwe2cllerq
================================================
FILE: demo/addons/gut/gut_loader.gd
================================================
# ------------------------------------------------------------------------------
# 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.
#
# Once you are ready to run tests, restore_ignore_addons should be called so
# that it has the expected value. This should be done after whatever loaded
# this is done loading and doing setup stuff.
#
# 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 test/resources/change_project_warnings.gd to
# quickly alter project warning levels for testing purposes.
# gdscript test/resources/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/'
static var were_addons_disabled : bool = true
@warning_ignore("unsafe_method_access")
@warning_ignore("unsafe_property_access")
@warning_ignore("untyped_declaration")
static func _static_init() -> void:
were_addons_disabled = ProjectSettings.get(str(WARNING_PATH, 'exclude_addons'))
ProjectSettings.set(str(WARNING_PATH, 'exclude_addons'), true)
var WarningsManager = load('res://addons/gut/warnings_manager.gd')
# Turn everything back on (if it originally was on) if the warnings manager
# is disabled. This makes sure we see all the warnings for all the scripts
# in the LazyLoader (except WarningsManager, but that's not a big deal).
#
# With the warnings manager disabled and all_warn warnings:
# test_warnings_manager.gd -> 5471 errors
# full run -> 131,742 errors
#
# With the warnings manager disabled and gut_default warnings:
# test_warnings_manager.gd -> 46 errors
# full run -> 165 errors.
if(WarningsManager.disabled):
ProjectSettings.set(str(WARNING_PATH, 'exclude_addons'), were_addons_disabled)
# 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()
# Make sure that the values set in WarningsManager's static_init actually
# reflect the project settings and not whatever we do here to make things
# not warn.
WarningsManager._project_warnings.exclude_addons = were_addons_disabled
# this can be called before tests are run to reinstate whatever exclude_addons
# was set to before this script disabled it.
static func restore_ignore_addons() -> void:
ProjectSettings.set(str(WARNING_PATH, 'exclude_addons'), were_addons_disabled)
# ##############################################################################
# (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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gut_loader.gd.uid
================================================
uid://2jdhrg7xws31
================================================
FILE: demo/addons/gut/gut_loader_the_scene.tscn
================================================
[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")
================================================
FILE: demo/addons/gut/gut_menu.gd
================================================
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():
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())
================================================
FILE: demo/addons/gut/gut_menu.gd.uid
================================================
uid://crhdyu6u7n8c4
================================================
FILE: demo/addons/gut/gut_plugin.gd
================================================
@tool
extends EditorPlugin
var VersionConversion = load("res://addons/gut/version_conversion.gd")
var MenuManager = load("res://addons/gut/gut_menu.gd")
var GutWindow = load("res://addons/gut/gui/GutEditorWindow.tscn")
var BottomPanelScene = preload('res://addons/gut/gui/GutBottomPanel.tscn')
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var _bottom_panel : Control = null
var _menu_mgr = null
var _gut_button = null
var _gut_window = null
var _dock_mode = 'none'
func _init():
if(VersionConversion.error_if_not_all_classes_imported()):
return
func _enter_tree():
if(!_version_conversion()):
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.
#
# 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
# ---
_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_window():
if(_gut_window == null):
_gut_window = GutWindow.instantiate()
_gut_window.gut_plugin = self
add_child(_gut_window)
_gut_window.theme = get_tree().root.theme
_gut_window.interface = get_editor_interface()
_gut_window.add_gut_panel(_bottom_panel)
_bottom_panel.make_floating_btn.visible = false
_gut_button = null
_dock_mode = 'window'
func gut_as_panel():
_gut_button = add_control_to_bottom_panel(_bottom_panel, 'GUT')
_bottom_panel.set_panel_button(_gut_button)
_gut_button.shortcut_in_tooltip = true
_dock_mode = 'panel'
_bottom_panel._apply_shortcuts()
_bottom_panel.results_horiz_layout()
_bottom_panel.make_floating_btn.visible = true
if(_gut_window != null):
_gut_window.queue_free()
_gut_window = null
func toggle_windowed():
_deparent_bottom_panel()
if(_dock_mode == 'window' or _dock_mode == 'none'):
gut_as_panel()
elif(_dock_mode == 'panel'):
gut_as_window()
_bottom_panel.show_me()
func _deparent_bottom_panel():
if(_dock_mode == 'window'):
_gut_window.remove_panel()
elif(_dock_mode == 'panel'):
remove_control_from_bottom_panel(_bottom_panel)
func _exit_tree():
remove_tool_menu_item("GUT")
_menu_mgr = null
GutEditorGlobals.user_prefs.save_it()
# Clean-up of the plugin goes here
# Always remember to remove_at it from the engine when deactivated
_deparent_bottom_panel()
if(_gut_window != null):
_gut_window.queue_free()
_bottom_panel.menu_manager = null
_bottom_panel.queue_free()
remove_tool_menu_item("GUT") # made by _menu_mgr
func show_output_panel():
if(_bottom_panel == null):
return
var panel = null
var kids = _bottom_panel.get_parent().get_children()
var idx = 0
while(idx < kids.size() and panel == null):
if(str(kids[idx]).contains(" String:
return str("CODE:", code, " TYPE:", error_type, " RATIONALE:", rationale, "\n",
file, '->', function, '@', line, "\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 != GutUtils.GUT_ERROR_TYPE and !is_push_error()
## 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_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)
================================================
FILE: demo/addons/gut/gut_tracked_error.gd.uid
================================================
uid://c1m2dbkoyf4fn
================================================
FILE: demo/addons/gut/gut_vscode_debugger.gd
================================================
# ------------------------------------------------------------------------------
# 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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/gut_vscode_debugger.gd.uid
================================================
uid://hflec26434u5
================================================
FILE: demo/addons/gut/hook_script.gd
================================================
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.")
## 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
================================================
FILE: demo/addons/gut/hook_script.gd.uid
================================================
uid://1i8mr2knypot
================================================
FILE: demo/addons/gut/inner_class_registry.gd
================================================
var _registry = {}
func _create_reg_entry(base_path, subpath):
var to_return = {
"base_path":base_path,
"subpath":subpath,
"base_resource":load(base_path),
"full_path":str("'", base_path, "'", subpath)
}
return to_return
func _register_inners(base_path, obj, prev_inner = ''):
var const_map = obj.get_script_constant_map()
var consts = const_map.keys()
var const_idx = 0
while(const_idx < consts.size()):
var key = consts[const_idx]
var thing = const_map[key]
if(typeof(thing) == TYPE_OBJECT and thing.resource_path == ''):
var cur_inner = str(prev_inner, ".", key)
_registry[thing] = _create_reg_entry(base_path, cur_inner)
_register_inners(base_path, thing, cur_inner)
const_idx += 1
func register(base_script):
var base_path = base_script.resource_path
_register_inners(base_path, base_script)
func get_extends_path(inner_class):
if(_registry.has(inner_class)):
return _registry[inner_class].full_path
else:
return null
# returns the subpath for the inner class. This includes the leading "." in
# the path.
func get_subpath(inner_class):
if(_registry.has(inner_class)):
return _registry[inner_class].subpath
else:
return ''
func get_base_path(inner_class):
if(_registry.has(inner_class)):
return _registry[inner_class].base_path
func has(inner_class):
return _registry.has(inner_class)
func get_base_resource(inner_class):
if(_registry.has(inner_class)):
return _registry[inner_class].base_resource
func to_s():
var text = ""
for key in _registry:
text += str(key, ": ", _registry[key], "\n")
return text
================================================
FILE: demo/addons/gut/inner_class_registry.gd.uid
================================================
uid://l4hh1hhgq3kx
================================================
FILE: demo/addons/gut/input_factory.gd
================================================
class_name GutInputFactory
## Static class full of helper methods to make InputEvent instances.
##
## This thing makes InputEvents. Enjoy.
# Implemented InputEvent* convenience methods
# InputEventAction
# InputEventKey
# InputEventMouseButton
# InputEventMouseMotion
# Yet to implement InputEvents
# InputEventJoypadButton
# InputEventJoypadMotion
# InputEventMagnifyGesture
# InputEventMIDI
# InputEventPanGesture
# InputEventScreenDrag
# InputEventScreenTouch
static func _to_scancode(which):
var key_code = which
if(typeof(key_code) == TYPE_STRING):
key_code = key_code.to_upper().to_ascii_buffer()[0]
return key_code
## Creates a new button with the given propoerties.
static func new_mouse_button_event(position, global_position, pressed, button_index) -> 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
# -----------
# ##############################################################################
================================================
FILE: demo/addons/gut/input_factory.gd.uid
================================================
uid://b4s8333o8s4cp
================================================
FILE: demo/addons/gut/input_sender.gd
================================================
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 InputQueueItem:
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 MouseDraw:
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 = MouseDraw.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 = InputQueueItem.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 = InputQueueItem.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(...)
#
# ##############################################################################
================================================
FILE: demo/addons/gut/input_sender.gd.uid
================================================
uid://da3cy6yko53dk
================================================
FILE: demo/addons/gut/junit_xml_export.gd
================================================
## Creates an export of a test run in the JUnit XML format.
##
## More words needed?
var _exporter = GutUtils.ResultExporter.new()
## @ignore should be private I think
func indent(s, ind):
var to_return = ind + s
to_return = to_return.replace("\n", "\n" + ind)
return to_return
# Wraps content in CDATA section because it may contain special characters
# e.g. str(null) becomes and can break XML parsing.
func wrap_cdata(content):
return ""
## @ignore should be private I think
func add_attr(name, value):
return str(name, '="', value, '" ')
func _export_test_result(test):
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, classname):
var to_return = ""
for key in script_result.keys():
var test = script_result[key]
var assert_count = test.passing.size() + test.failing.size()
to_return += "\n"
to_return += _export_test_result(test)
to_return += "\n"
return to_return
func _sum_test_time(script_result, classname)->float:
var to_return := 0.0
for key in script_result.keys():
var test = script_result[key]
to_return += test.time_taken
return to_return
func _export_scripts(exp_results):
var to_return = ""
for key in exp_results.test_scripts.scripts.keys():
var s = exp_results.test_scripts.scripts[key]
to_return += "\n"
to_return += indent(_export_tests(s.tests, key), " ")
to_return += "\n"
return to_return
## Takes in an instance of GutMain and returns a string of XML representing the
## results of the run.
func get_results_xml(gut):
var exp_results = _exporter.get_results_dictionary(gut)
var to_return = '' + "\n"
to_return += '\n"
to_return += indent(_export_scripts(exp_results), " ")
to_return += ''
return to_return
## Takes in an instance of GutMain and writes the XML file to the specified
## path
func write_file(gut, path):
var xml = get_results_xml(gut)
var f_result = 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
================================================
FILE: demo/addons/gut/junit_xml_export.gd.uid
================================================
uid://23m7uwtgs8nj
================================================
FILE: demo/addons/gut/lazy_loader.gd
================================================
@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
================================================
FILE: demo/addons/gut/lazy_loader.gd.uid
================================================
uid://rcaa7rmoaaub
================================================
FILE: demo/addons/gut/logger.gd
================================================
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='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='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.TerminalPrinter.new()
_printers.console = GutUtils.Printers.ConsolePrinter.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)
# 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).
# ##############################################################################
================================================
FILE: demo/addons/gut/logger.gd.uid
================================================
uid://cqd3rmuu2drug
================================================
FILE: demo/addons/gut/menu_manager.gd.uid
================================================
uid://b3k81q304cji1
================================================
FILE: demo/addons/gut/method_maker.gd
================================================
class CallParameters:
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 []
var _lgr = GutUtils.get_logger()
const PARAM_PREFIX = 'p_'
# ------------------------------------------------------
# _supported_defaults
#
# This array contains all the data types that are supported for default values.
# If a value is supported it will contain either an empty string or a prefix
# that should be used when setting the parameter default value.
# For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false)
# but things like Vectors and Colors do since only the parameters to create a
# new Vector or Color are included in the metadata.
# ------------------------------------------------------
# TYPE_NIL = 0 — Variable is of type nil (only applied for null).
# TYPE_BOOL = 1 — Variable is of type bool.
# TYPE_INT = 2 — Variable is of type int.
# TYPE_FLOAT = 3 — Variable is of type float/real.
# TYPE_STRING = 4 — Variable is of type String.
# TYPE_VECTOR2 = 5 — Variable is of type Vector2.
# TYPE_RECT2 = 6 — Variable is of type Rect2.
# TYPE_VECTOR3 = 7 — Variable is of type Vector3.
# TYPE_COLOR = 14 — Variable is of type Color.
# TYPE_OBJECT = 17 — Variable is of type Object.
# TYPE_DICTIONARY = 18 — Variable is of type Dictionary.
# TYPE_ARRAY = 19 — Variable is of type Array.
# TYPE_PACKED_VECTOR2_ARRAY = 24 — Variable is of type PackedVector2Array.
# TYPE_TRANSFORM3D = 13 — Variable is of type Transform3D.
# TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D.
# TYPE_RID = 16 — Variable is of type RID.
# TYPE_PACKED_INT32_ARRAY = 21 — Variable is of type PackedInt32Array.
# TYPE_PACKED_FLOAT32_ARRAY = 22 — Variable is of type PackedFloat32Array.
# TYPE_PACKED_STRING_ARRAY = 23 — Variable is of type PackedStringArray.
# TYPE_PLANE = 9 — Variable is of type Plane.
# TYPE_QUATERNION = 10 — Variable is of type Quaternion.
# TYPE_AABB = 11 — Variable is of type AABB.
# TYPE_BASIS = 12 — Variable is of type Basis.
# TYPE_NODE_PATH = 15 — Variable is of type NodePath.
# TYPE_PACKED_BYTE_ARRAY = 20 — Variable is of type PackedByteArray.
# TYPE_PACKED_VECTOR3_ARRAY = 25 — Variable is of type PackedVector3Array.
# TYPE_PACKED_COLOR_ARRAY = 26 — Variable is of type PackedColorArray.
# TYPE_MAX = 27 — Marker for end of type constants.
# ------------------------------------------------------
var _supported_defaults = []
func _init():
for _i in range(TYPE_MAX):
_supported_defaults.append(null)
# These types do not require a prefix for defaults
_supported_defaults[TYPE_NIL] = ''
_supported_defaults[TYPE_BOOL] = ''
_supported_defaults[TYPE_INT] = ''
_supported_defaults[TYPE_FLOAT] = ''
_supported_defaults[TYPE_OBJECT] = ''
_supported_defaults[TYPE_ARRAY] = ''
_supported_defaults[TYPE_STRING] = ''
_supported_defaults[TYPE_STRING_NAME] = ''
_supported_defaults[TYPE_DICTIONARY] = ''
_supported_defaults[TYPE_PACKED_VECTOR2_ARRAY] = ''
_supported_defaults[TYPE_RID] = ''
# These require a prefix for whatever default is provided
_supported_defaults[TYPE_VECTOR2] = 'Vector2'
_supported_defaults[TYPE_VECTOR2I] = 'Vector2i'
_supported_defaults[TYPE_RECT2] = 'Rect2'
_supported_defaults[TYPE_RECT2I] = 'Rect2i'
_supported_defaults[TYPE_VECTOR3] = 'Vector3'
_supported_defaults[TYPE_COLOR] = 'Color'
_supported_defaults[TYPE_TRANSFORM2D] = 'Transform2D'
_supported_defaults[TYPE_TRANSFORM3D] = 'Transform3D'
_supported_defaults[TYPE_PACKED_INT32_ARRAY] = 'PackedInt32Array'
_supported_defaults[TYPE_PACKED_FLOAT32_ARRAY] = 'PackedFloat32Array'
_supported_defaults[TYPE_PACKED_STRING_ARRAY] = 'PackedStringArray'
# ###############
# Private
# ###############
var _func_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/function_template.txt')
var _init_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/init_template.txt')
func _is_supported_default(type_flag):
return type_flag >= 0 and type_flag < _supported_defaults.size() and _supported_defaults[type_flag] != null
func _make_stub_default(method, index):
return str('__gutdbl.default_val("', method, '",', index, ')')
func _make_arg_array(method_meta):
var to_return = []
var has_unsupported_defaults = false
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(CallParameters.new(PARAM_PREFIX + pname, dflt_text))
if(method_meta.flags & METHOD_FLAG_VARARG):
var cp = CallParameters.new("args", "")
cp.vararg = true
to_return.append(cp)
return [has_unsupported_defaults, 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(method_name, args):
var params = ''
for i in range(args.size()):
params += args[i].p_name
if(i != args.size() -1):
params += ', '
return str('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
# ###############
# Public
# ###############
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_text.format({
"func_decleration":decleration,
"super_params":super_params,
"param_array":param_array,
"method_name":meta.name,
})
return text
# 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(meta, override_size=null):
var method_params = ''
var text = null
var result = _make_arg_array(meta)
var has_unsupported = result[0]
var args = result[1]
var param_array = _get_spy_call_parameters_text(args)
if(has_unsupported):
# This will cause a runtime error. This is the most convenient way to
# to stop running before the error gets more obscure. _make_arg_array
# generates a gut error when unsupported defaults are found.
method_params = null
else:
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 decleration = str('func ', meta.name, '(', method_params, '):')
text = _func_text.format({
"func_decleration":decleration,
"method_name":meta.name,
"param_array":param_array,
"super_call":_get_super_call_text(meta.name, args),
})
return text
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
================================================
FILE: demo/addons/gut/method_maker.gd.uid
================================================
uid://c6fxaf7bsbrp7
================================================
FILE: demo/addons/gut/one_to_many.gd
================================================
# ------------------------------------------------------------------------------
# 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".
# ------------------------------------------------------------------------------
var items = {}
# 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(!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
================================================
FILE: demo/addons/gut/one_to_many.gd.uid
================================================
uid://cy3q0wsdgq515
================================================
FILE: demo/addons/gut/orphan_counter.gd
================================================
# ------------------------------------------------------------------------------
# It keeps track of the orphans...so this is best name it could ever have.
# ------------------------------------------------------------------------------
class Orphanage:
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 : Orphanage = Orphanage.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.
#
# ##############################################################################
================================================
FILE: demo/addons/gut/orphan_counter.gd.uid
================================================
uid://cgoysoitlbudy
================================================
FILE: demo/addons/gut/parameter_factory.gd
================================================
## 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.
# ##############################################################################
================================================
FILE: demo/addons/gut/parameter_factory.gd.uid
================================================
uid://c0e08oi55x8qj
================================================
FILE: demo/addons/gut/parameter_handler.gd
================================================
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()
================================================
FILE: demo/addons/gut/parameter_handler.gd.uid
================================================
uid://ba87ra5ep18wa
================================================
FILE: demo/addons/gut/plugin.cfg
================================================
[plugin]
name="Gut"
description="Unit Testing tool for Godot."
author="Butch Wesley"
version="9.5.0"
script="gut_plugin.gd"
================================================
FILE: demo/addons/gut/printers.gd
================================================
# ------------------------------------------------------------------------------
# Interface and some basic functionality for all printers.
# ------------------------------------------------------------------------------
class Printer:
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 Printer
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 ConsolePrinter:
extends Printer
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 TerminalPrinter:
extends Printer
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'))
================================================
FILE: demo/addons/gut/printers.gd.uid
================================================
uid://nijvqplhkwjc
================================================
FILE: demo/addons/gut/result_exporter.gd
================================================
# ------------------------------------------------------------------------------
# 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
================================================
FILE: demo/addons/gut/result_exporter.gd.uid
================================================
uid://bbto5glvw8efv
================================================
FILE: demo/addons/gut/script_parser.gd
================================================
# 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',
]
# ------------------------------------------------------------------------------
# Combins 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 ParsedMethod:
const NO_DEFAULT = '__no__default__'
var _meta = {}
var meta = _meta :
get: return _meta
set(val): return;
var is_local = false
var _parameters = []
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
_parameters.append(arg)
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 ParsedScript:
# 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 = ""
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
var inst = to_load.new()
_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 = ParsedMethod.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()
for m in methods:
var parsed_method = ParsedMethod.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 = ParsedScript.new(obj, inner)
scripts[key] = parsed
return parsed
================================================
FILE: demo/addons/gut/script_parser.gd.uid
================================================
uid://c4k82nmegjoec
================================================
FILE: demo/addons/gut/signal_watcher.gd
================================================
# ##############################################################################
# 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)
================================================
FILE: demo/addons/gut/signal_watcher.gd.uid
================================================
uid://yj7vo3wcr68q
================================================
FILE: demo/addons/gut/spy.gd
================================================
# {
# instance_id_or_path1:{
# method1:[ [p1, p2], [p1, p2] ],
# method2:[ [p1, p2], [p1, p2] ]
# },
# instance_id_or_path1:{
# method1:[ [p1, p2], [p1, p2] ],
# method2:[ [p1, p2], [p1, p2] ]
# },
# }
var _calls = {}
var _lgr = GutUtils.get_logger()
var _compare = GutUtils.Comparator.new()
func _find_parameters(call_params, params_to_find):
var found = false
var idx = 0
while(idx < call_params.size() and !found):
var result = _compare.deep(call_params[idx], params_to_find)
if(result.are_equal):
found = true
else:
idx += 1
return found
func _get_params_as_string(params):
var to_return = ''
if(params == null):
return ''
for i in range(params.size()):
if(params[i] == null):
to_return += 'null'
else:
if(typeof(params[i]) == TYPE_STRING):
to_return += str('"', params[i], '"')
else:
to_return += str(params[i])
if(i != params.size() -1):
to_return += ', '
return to_return
func add_call(variant, method_name, parameters=null):
if(!_calls.has(variant)):
_calls[variant] = {}
if(!_calls[variant].has(method_name)):
_calls[variant][method_name] = []
_calls[variant][method_name].append(parameters)
func was_called(variant, method_name, parameters=null):
var to_return = false
if(_calls.has(variant) and _calls[variant].has(method_name)):
if(parameters):
to_return = _find_parameters(_calls[variant][method_name], parameters)
else:
to_return = true
return to_return
func get_call_parameters(variant, method_name, index=-1):
var to_return = null
var get_index = -1
if(_calls.has(variant) and _calls[variant].has(method_name)):
var call_size = _calls[variant][method_name].size()
if(index == -1):
# get the most recent call by default
get_index = call_size -1
else:
get_index = index
if(get_index < call_size):
to_return = _calls[variant][method_name][get_index]
else:
_lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls: ', call_size))
return to_return
func call_count(instance, method_name, parameters=null):
var to_return = 0
if(was_called(instance, method_name)):
if(parameters):
for i in range(_calls[instance][method_name].size()):
if(_calls[instance][method_name][i] == parameters):
to_return += 1
else:
to_return = _calls[instance][method_name].size()
return to_return
func clear():
_calls = {}
func get_call_list_as_string(instance):
var to_return = ''
if(_calls.has(instance)):
for method in _calls[instance]:
for i in range(_calls[instance][method].size()):
to_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), ")\n")
return to_return
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
================================================
FILE: demo/addons/gut/spy.gd.uid
================================================
uid://blj7je6n53r51
================================================
FILE: demo/addons/gut/strutils.gd
================================================
class_name GutStringUtils
# Hash containing all the built in types in Godot. This provides an English
# name for the types that corosponds with the type constants defined in the
# engine.
var types = {}
func _init_types_dictionary():
types[TYPE_NIL] = 'NIL'
types[TYPE_AABB] = 'AABB'
types[TYPE_ARRAY] = 'ARRAY'
types[TYPE_BASIS] = 'BASIS'
types[TYPE_BOOL] = 'BOOL'
types[TYPE_CALLABLE] = 'CALLABLE'
types[TYPE_COLOR] = 'COLOR'
types[TYPE_DICTIONARY] = 'DICTIONARY'
types[TYPE_FLOAT] = 'FLOAT'
types[TYPE_INT] = 'INT'
types[TYPE_MAX] = 'MAX'
types[TYPE_NODE_PATH] = 'NODE_PATH'
types[TYPE_OBJECT] = 'OBJECT'
types[TYPE_PACKED_BYTE_ARRAY] = 'PACKED_BYTE_ARRAY'
types[TYPE_PACKED_COLOR_ARRAY] = 'PACKED_COLOR_ARRAY'
types[TYPE_PACKED_FLOAT32_ARRAY] = 'PACKED_FLOAT32_ARRAY'
types[TYPE_PACKED_FLOAT64_ARRAY] = 'PACKED_FLOAT64_ARRAY'
types[TYPE_PACKED_INT32_ARRAY] = 'PACKED_INT32_ARRAY'
types[TYPE_PACKED_INT64_ARRAY] = 'PACKED_INT64_ARRAY'
types[TYPE_PACKED_STRING_ARRAY] = 'PACKED_STRING_ARRAY'
types[TYPE_PACKED_VECTOR2_ARRAY] = 'PACKED_VECTOR2_ARRAY'
types[TYPE_PACKED_VECTOR3_ARRAY] = 'PACKED_VECTOR3_ARRAY'
types[TYPE_PLANE] = 'PLANE'
types[TYPE_PROJECTION] = 'PROJECTION'
types[TYPE_QUATERNION] = 'QUATERNION'
types[TYPE_RECT2] = 'RECT2'
types[TYPE_RECT2I] = 'RECT2I'
types[TYPE_RID] = 'RID'
types[TYPE_SIGNAL] = 'SIGNAL'
types[TYPE_STRING_NAME] = 'STRING_NAME'
types[TYPE_STRING] = 'STRING'
types[TYPE_TRANSFORM2D] = 'TRANSFORM2D'
types[TYPE_TRANSFORM3D] = 'TRANSFORM3D'
types[TYPE_VECTOR2] = 'VECTOR2'
types[TYPE_VECTOR2I] = 'VECTOR2I'
types[TYPE_VECTOR3] = 'VECTOR3'
types[TYPE_VECTOR3I] = 'VECTOR3I'
types[TYPE_VECTOR4] = 'VECTOR4'
types[TYPE_VECTOR4I] = 'VECTOR4I'
# Types to not be formatted when using _str
var _str_ignore_types = [
TYPE_INT, TYPE_FLOAT, TYPE_STRING,
TYPE_NIL, TYPE_BOOL
]
func _init():
_init_types_dictionary()
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _get_filename(path):
return path.split('/')[-1]
# ------------------------------------------------------------------------------
# Gets the filename of an object passed in. This does not return the
# full path to the object, just the filename.
# ------------------------------------------------------------------------------
func _get_obj_filename(thing):
var filename = null
if(thing == null or
GutUtils.is_native_class(thing) or
!is_instance_valid(thing) or
str(thing) == '