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 ================================================ ![logo](docs/src/doc/assets/fmod-gdextension-logo.png) [![🌈 Build](https://github.com/utopia-rise/fmod-gdextension/actions/workflows/release.yml/badge.svg)](https://github.com/utopia-rise/fmod-gdextension/actions/workflows/release.yml) [![](https://img.shields.io/discord/1012326818365325352.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](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. ![fmod-events](docs/src/doc/assets/screenshot-01.png) ## 🔉 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. ![fmod-nodes](docs/src/doc/assets/screenshot-02.png) # 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 += "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 += " 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) == '' or typeof(thing) != TYPE_OBJECT or GutUtils.is_double(thing)): return if(thing.get_script() == null): if(thing is PackedScene): filename = _get_filename(thing.resource_path) else: # If it isn't a packed scene and it doesn't have a script then # we do nothing. This just reads better. pass elif(!GutUtils.is_native_class(thing)): var dict = inst_to_dict(thing) filename = _get_filename(dict['@path']) if(str(dict['@subpath']) != ''): filename += str('/', dict['@subpath']) return filename # ------------------------------------------------------------------------------ # Better object/thing to string conversion. Includes extra details about # whatever is passed in when it can/should. # ------------------------------------------------------------------------------ func type2str(thing): var filename = _get_obj_filename(thing) var str_thing = str(thing) if(thing == null): # According to str there is a difference between null and an Object # that is somehow null. To avoid getting '[Object:null]' as output # always set it to str(null) instead of str(thing). A null object # will pass typeof(thing) == TYPE_OBJECT check so this has to be # before that. str_thing = str(null) elif(typeof(thing) == TYPE_FLOAT): if(!'.' in str_thing): str_thing += '.0' elif(typeof(thing) == TYPE_STRING): str_thing = str('"', thing, '"') elif(typeof(thing) in _str_ignore_types): # do nothing b/c we already have str(thing) in # to_return. I think this just reads a little # better this way. pass elif(typeof(thing) == TYPE_OBJECT): if(GutUtils.is_native_class(thing)): str_thing = GutUtils.get_native_class_name(thing) elif(GutUtils.is_double(thing)): var double_path = _get_filename(thing.__gutdbl.thepath) if(thing.__gutdbl.subpath != ''): double_path += str('/', thing.__gutdbl.subpath) elif(thing.__gutdbl.from_singleton != ''): double_path = thing.__gutdbl.from_singleton + " Singleton" var double_type = "double" if(thing.__gutdbl.is_partial): double_type = "partial-double" str_thing += str("(", double_type, " of ", double_path, ")") filename = null elif(types.has(typeof(thing))): if(!str_thing.begins_with('(')): str_thing = '(' + str_thing + ')' str_thing = str(types[typeof(thing)], str_thing) if(filename != null): str_thing += str('(', filename, ')') return str_thing # ------------------------------------------------------------------------------ # Returns the string truncated with an '...' in it. Shows the start and last # 10 chars. If the string is smaller than max_size the entire string is # returned. If max_size is -1 then truncation is skipped. # ------------------------------------------------------------------------------ func truncate_string(src, max_size): var to_return = src if(src.length() > max_size - 10 and max_size != -1): to_return = str(src.substr(0, max_size - 10), '...', src.substr(src.length() - 10, src.length())) return to_return func _get_indent_text(times, pad): var to_return = '' for i in range(times): to_return += pad return to_return func indent_text(text, times, pad): if(times == 0): return text var to_return = text var ending_newline = '' if(text.ends_with("\n")): ending_newline = "\n" to_return = to_return.left(to_return.length() -1) var padding = _get_indent_text(times, pad) to_return = to_return.replace("\n", "\n" + padding) to_return += ending_newline return padding + to_return ================================================ FILE: demo/addons/gut/strutils.gd.uid ================================================ uid://dntjtiq2ppvmq ================================================ FILE: demo/addons/gut/stub_params.gd ================================================ var _is_return_override = false var _is_defaults_override = false var _is_call_override = false var _method_meta : Dictionary = {} var _lgr = GutUtils.get_logger() var logger = _lgr : get: return _lgr set(val): _lgr = val var return_val = null var stub_target = null var parameters = null # the parameter values to match method call on. var stub_method = null var call_super = false var call_this = null # Whether this is a stub for default parameter values as they are defined in # the script, and not an overridden default value. var is_script_default = false var parameter_count = -1 : get(): _lgr.deprecated("parameter count deprecated") return -1 # Default values for parameters. This is used to store default values for # scripts and to override those values. I'm not sure if there is a need to # override them anymore, since I think this was introduced for stubbing vararg # methods, but you still can for now. This value should only be used if # is_defaults_override is true. var parameter_defaults = [] const NOT_SET = '|_1_this_is_not_set_1_|' func _init(target=null, method=null, _subpath=null): stub_target = target stub_method = method if(typeof(target) == TYPE_CALLABLE): stub_target = target.get_object() stub_method = target.get_method() parameters = target.get_bound_arguments() if(parameters.size() == 0): parameters = null elif(typeof(target) == TYPE_STRING): if(target.is_absolute_path()): stub_target = load(str(target)) else: _lgr.warn(str(target, ' is not a valid path')) if(stub_target is PackedScene): stub_target = GutUtils.get_scene_script_object(stub_target) # this is used internally to stub default parameters for everything that is # doubled...or something. Look for stub_defaults_from_meta for usage. This # behavior is not to be used by end users. if(typeof(method) == TYPE_DICTIONARY): _method_meta = method _load_defaults_from_metadata(method) is_script_default = true func _load_defaults_from_metadata(meta): stub_method = meta.name var values = meta.default_args.duplicate() while (values.size() < meta.args.size()): values.push_front(null) param_defaults(values) func _get_method_meta(): if(_method_meta == {} and typeof(stub_target) == TYPE_OBJECT): var found_meta = GutUtils.get_method_meta(stub_target, stub_method) if(found_meta != null): _method_meta = found_meta return _method_meta # ------------------------- # Public # ------------------------- func to_return(val): return_val = val call_super = false _is_return_override = true return self func to_do_nothing(): to_return(null) return self func to_call_super(): call_super = true _is_call_override = true return self func to_call(callable : Callable): call_this = callable _is_call_override = true return self func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET): parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] var idx = 0 while(idx < parameters.size()): if(str(parameters[idx]) == NOT_SET): parameters.remove_at(idx) else: idx += 1 return self func param_count(_x): _lgr.deprecated("Stubbing param_count is no longer required or supported.") return self func param_defaults(values): var meta = _get_method_meta() if(meta != {} and meta.flags & METHOD_FLAG_VARARG): _lgr.error("Cannot stub defaults for methods with varargs.") else: parameter_defaults = values _is_defaults_override = true return self func is_default_override_only(): return is_defaults_override() and !is_return_override() and !is_call_override() func is_return_override(): return _is_return_override func is_defaults_override(): return _is_defaults_override func is_call_override(): return _is_call_override func to_s(): var base_string = str(stub_target, '.', stub_method) if(parameter_defaults.size() > 0): base_string += str(" defaults ", parameter_defaults) if(call_super): base_string += " to call SUPER" if(call_this != null): base_string += str(" to call ", call_this) if(parameters != null): base_string += str(' with params (', parameters, ') returns ', return_val) else: base_string += str(' returns ', return_val) return base_string ================================================ FILE: demo/addons/gut/stub_params.gd.uid ================================================ uid://dblktyc7hyt4f ================================================ FILE: demo/addons/gut/stubber.gd ================================================ static var _class_db_name_hash = {} : get(): if(_class_db_name_hash == {}): _class_db_name_hash = _make_crazy_dynamic_over_engineered_class_db_hash() return _class_db_name_hash # So, I couldn't figure out how to get to a reference for a GDNative Class # using a string. ClassDB has all thier names...so I made a hash using those # names and the classes. Then I dynmaically make a script that has that as # the source and grab the hash out of it and return it. Super Rube Golbergery, # but tons of fun. static func _make_crazy_dynamic_over_engineered_class_db_hash(): var text = "var all_the_classes: Dictionary = {\n" for classname in ClassDB.get_class_list(): if(ClassDB.can_instantiate(classname)): text += str('"', classname, '": ', classname, ", \n") else: text += str('# ', classname, "\n") text += "}" var inst = GutUtils.create_script_from_source(text).new() return inst.all_the_classes # ------------- # returns{} and parameters {} have the followin structure # ------------- # { # inst_id_or_path1:{ # method_name1: [StubParams, StubParams], # method_name2: [StubParams, StubParams] # }, # inst_id_or_path2:{ # method_name1: [StubParams, StubParams], # method_name2: [StubParams, StubParams] # } # } var returns = {} var _lgr = GutUtils.get_logger() var _strutils = GutUtils.Strutils.new() func _find_matches(obj, method): var matches = [] var last_not_null_parent = null # Search for what is passed in first. This could be a class or an instance. # We want to find the instance before we find the class. If we do not have # an entry for the instance then see if we have an entry for the class. if(returns.has(obj) and returns[obj].has(method)): matches = returns[obj][method] elif(GutUtils.is_instance(obj)): var parent = obj.get_script() var found = false while(parent != null and !found): found = returns.has(parent) if(!found): last_not_null_parent = parent parent = parent.get_base_script() # Could not find the script so check to see if a native class of this # type was stubbed. if(!found): var base_type = last_not_null_parent.get_instance_base_type() if(_class_db_name_hash.has(base_type)): parent = _class_db_name_hash[base_type] found = returns.has(parent) if(found and returns[parent].has(method)): matches = returns[parent][method] return matches # Searches returns for an entry that matches the instance or the class that # passed in obj is. # # obj can be an instance, class, or a path. func _find_stub(obj, method, parameters=null, find_overloads=false): var to_return = null var matches = _find_matches(obj, method) if(matches.size() == 0): return null var param_match = null var null_match = null var overload_match = null for i in range(matches.size()): var cur_stub = matches[i] if(cur_stub.parameters == parameters): param_match = cur_stub if(cur_stub.parameters == null and !cur_stub.is_default_override_only()): null_match = cur_stub if(cur_stub.is_defaults_override): if(overload_match == null || overload_match.is_script_default): overload_match = cur_stub if(find_overloads and overload_match != null): to_return = overload_match # We have matching parameter values so return the stub value for that elif(param_match != null): to_return = param_match # We found a case where the parameters were not specified so return # parameters for that. Only do this if the null match is not *just* # a paramerter override stub. elif(null_match != null): to_return = null_match return to_return # ############## # Public # ############## func add_stub(stub_params): stub_params._lgr = _lgr var key = stub_params.stub_target if(!returns.has(key)): returns[key] = {} if(!returns[key].has(stub_params.stub_method)): returns[key][stub_params.stub_method] = [] returns[key][stub_params.stub_method].append(stub_params) # Gets a stubbed return value for the object and method passed in. If the # instance was stubbed it will use that, otherwise it will use the path and # subpath of the object to try to find a value. # # It will also use the optional list of parameter values to find a value. If # the object was stubbed with no parameters than any parameters will match. # If it was stubbed with specific parameter values then it will try to match. # If the parameters do not match BUT there was also an empty parameter list stub # then it will return those. # If it cannot find anything that matches then null is returned.for # # Parameters # obj: this should be an instance of a doubled object. # method: the method called # parameters: optional array of parameter vales to find a return value for. func get_return(obj, method, parameters=null): var stub_info = _find_stub(obj, method, parameters) if(stub_info != null): return stub_info.return_val else: _lgr.info(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.')) return null func should_call_super(obj, method, parameters=null): var stub_info = _find_stub(obj, method, parameters) var is_partial = false if(typeof(obj) != TYPE_STRING): # some stubber tests test with strings is_partial = obj.__gutdbl.is_partial var should = is_partial if(stub_info != null): should = stub_info.call_super elif(!is_partial): # this log message is here because of how the generated doubled scripts # are structured. With this log msg here, you will only see one # "unstubbed" info instead of multiple. _lgr.info('Unstubbed call to ' + method + '::' + _strutils.type2str(obj)) should = false return should func get_call_this(obj, method, parameters=null): var stub_info = _find_stub(obj, method, parameters) if(stub_info != null): return stub_info.call_this func get_default_value(obj, method, p_index): var matches = _find_matches(obj, method) var the_defaults = [] var script_defaults = [] var i = matches.size() -1 while(i >= 0 and the_defaults.is_empty()): if(matches[i].is_defaults_override()): if(matches[i].is_script_default): script_defaults = matches[i].parameter_defaults else: the_defaults = matches[i].parameter_defaults i -= 1 if(the_defaults.is_empty() and !script_defaults.is_empty()): the_defaults = script_defaults var to_return = null if(the_defaults.size() > p_index): to_return = the_defaults[p_index] return to_return func clear(): returns.clear() func get_logger(): return _lgr func set_logger(logger): _lgr = logger func to_s(): var text = '' for thing in returns: text += str("-- ", thing, " --\n") for method in returns[thing]: text += str("\t", method, "\n") for i in range(returns[thing][method].size()): text += "\t\t" + returns[thing][method][i].to_s() + "\n" if(text == ''): text = 'Stubber is empty'; return text func stub_defaults_from_meta(target, method_meta): var params = GutUtils.StubParams.new(target, method_meta) params.is_script_default = true add_stub(params) ================================================ FILE: demo/addons/gut/stubber.gd.uid ================================================ uid://cjrpyjnk8lkq8 ================================================ FILE: demo/addons/gut/summary.gd ================================================ # ------------------------------------------------------------------------------ # Prints things, mostly. Knows too much about gut.gd, but it's only supposed to # work with gut.gd, so I'm fine with that. # ------------------------------------------------------------------------------ # a _test_collector to use when one is not provided. var _gut = null func _init(gut=null): _gut = gut # --------------------- # Private # --------------------- func _log_end_run_header(gut): var lgr = gut.get_logger() lgr.log('==============================================', lgr.fmts.yellow) lgr.log("= Run Summary", lgr.fmts.yellow) lgr.log('==============================================', lgr.fmts.yellow) func _log_what_was_run(gut): if(!GutUtils.is_null_or_empty(gut._select_script)): gut.p('Ran Scripts matching "' + gut._select_script + '"') if(!GutUtils.is_null_or_empty(gut._unit_test_name)): gut.p('Ran Tests matching "' + gut._unit_test_name + '"') if(!GutUtils.is_null_or_empty(gut._inner_class_name)): gut.p('Ran Inner Classes matching "' + gut._inner_class_name + '"') func _total_fmt(text, value): var space = 18 if(str(value) == '0'): value = 'none' return str(text.rpad(space), str(value).lpad(5)) func _log_non_zero_total(text, value, lgr): if(str(value) != '0'): lgr.log(_total_fmt(text, value)) return 1 else: return 0 func _log_totals(gut, totals): var lgr = gut.get_logger() lgr.log() # lgr.log("---- Totals ----") lgr.log("Totals") lgr.log("------") var issue_count = 0 issue_count += _log_non_zero_total('Errors', totals.errors, lgr) issue_count += _log_non_zero_total('Warnings', totals.warnings, lgr) issue_count += _log_non_zero_total('Deprecated', totals.deprecated, lgr) if(issue_count > 0): lgr.log("") lgr.log(_total_fmt( 'Scripts', totals.scripts)) lgr.log(_total_fmt( 'Tests', gut.get_test_collector().get_ran_test_count())) lgr.log(_total_fmt( 'Passing Tests', totals.passing_tests)) _log_non_zero_total('Failing Tests', totals.failing_tests, lgr) _log_non_zero_total('Risky/Pending', totals.risky + totals.pending, lgr) if(totals.failing == 0): lgr.log(_total_fmt( 'Asserts', totals.passing + totals.failing)) else: lgr.log(_total_fmt( 'Asserts', str(totals.passing, '/', totals.passing + totals.failing))) _log_non_zero_total( 'Orphans', totals.orphans, lgr) lgr.log(_total_fmt( 'Time', str(gut.get_elapsed_time(), 's'))) return totals func _log_nothing_run(gut): var lgr = gut.get_logger() lgr.error("Nothing was run.") lgr.log('On the one hand nothing failed, on the other hand nothing did anything.') _log_what_was_run(gut) # --------------------- # Public # --------------------- func log_all_non_passing_tests(gut=_gut): var test_collector = gut.get_test_collector() var lgr = gut.get_logger() var to_return = { passing = 0, non_passing = 0 } for test_script in test_collector.scripts: lgr.set_indent_level(0) if(test_script.was_skipped or test_script.get_fail_count() > 0 or test_script.get_pending_count() > 0): lgr.log("\n" + test_script.get_full_name(), lgr.fmts.underline) if(test_script.was_skipped): lgr.inc_indent() var skip_msg = str('[Risky] Script was skipped: ', test_script.skip_reason) lgr.log(skip_msg, lgr.fmts.yellow) lgr.dec_indent() var test_fail_count = 0 for test in test_script.tests: if(test.was_run): if(test.is_passing()): to_return.passing += 1 else: to_return.non_passing += 1 lgr.log(str('- ', test.name)) lgr.inc_indent() for i in range(test.fail_texts.size()): lgr.failed(test.fail_texts[i]) test_fail_count += 1 for i in range(test.pending_texts.size()): lgr.pending(test.pending_texts[i]) if(test.is_risky()): lgr.risky('Did not assert') lgr.dec_indent() if(test_script.get_fail_count() > test_fail_count): lgr.failed("before_all/after_all assert failed") return to_return func log_the_final_line(totals, gut): var lgr = gut.get_logger() var grand_total_text = "" var grand_total_fmt = lgr.fmts.none if(totals.failing_tests > 0): grand_total_text = str(totals.failing_tests, " failing tests") grand_total_fmt = lgr.fmts.red elif(totals.failing > 0): # no failing tests, but some failing asserts grand_total_text = str(totals.failing, " assert(s) in before_all/after_all methods failed") grand_total_fmt = lgr.fmts.red elif(totals.risky > 0 or totals.pending > 0): grand_total_text = str(totals.risky + totals.pending, " pending/risky tests.") grand_total_fmt = lgr.fmts.yellow else: grand_total_text = "All tests passed!" grand_total_fmt = lgr.fmts.green lgr.log(str("---- ", grand_total_text, " ----"), grand_total_fmt) func log_totals(gut, totals): var lgr = gut.get_logger() var orig_indent = lgr.get_indent_level() lgr.set_indent_level(0) _log_totals(gut, totals) lgr.set_indent_level(orig_indent) func get_totals(gut=_gut): var tc = gut.get_test_collector() var lgr = gut.get_logger() var totals = { failing = 0, failing_tests = 0, passing = 0, passing_tests = 0, pending = 0, risky = 0, scripts = tc.get_ran_script_count(), tests = 0, deprecated = lgr.get_deprecated().size(), errors = lgr.get_errors().size(), warnings = lgr.get_warnings().size(), } for s in tc.scripts: # assert totals totals.passing += s.get_pass_count() totals.pending += s.get_pending_count() totals.failing += s.get_fail_count() # test totals totals.tests += s.get_ran_test_count() totals.passing_tests += s.get_passing_test_count() totals.failing_tests += s.get_failing_test_count() totals.risky += s.get_risky_count() totals.orphans = gut.get_orphan_counter().orphan_count() return totals func log_end_run(gut=_gut): var totals = get_totals(gut) if(totals.tests == 0): _log_nothing_run(gut) return _log_end_run_header(gut) var lgr = gut.get_logger() log_all_non_passing_tests(gut) log_totals(gut, totals) lgr.log("\n") _log_what_was_run(gut) log_the_final_line(totals, gut) lgr.log("") ================================================ FILE: demo/addons/gut/summary.gd.uid ================================================ uid://byxw2c883i2kr ================================================ FILE: demo/addons/gut/test.gd ================================================ class_name GutTest extends Node ## This is the base class for your GUT test scripts.[br] ## [br] ## GUT Wiki: [url=https://gut.readthedocs.io]https://gut.readthedocs.io[/url] ## [br] ## Simple Example ## [codeblock] ## extends GutTest ## ## func before_all(): ## gut.p("before_all called" ## ## func before_each(): ## gut.p("before_each called") ## ## func after_each(): ## gut.p("after_each called") ## ## func after_all(): ## gut.p("after_all called") ## ## func test_assert_eq_letters(): ## assert_eq("asdf", "asdf", "Should pass") ## ## func test_assert_eq_number_not_equal(): ## assert_eq(1, 2, "Should fail. 1 != 2") ## [/codeblock] # Normalizes p1 and p2 into object/signal_name/signal_ref(sig). Additional # parameters are optional and will be placed into the others array. This # class is used in refactoring signal methods to accept a reference to the # signal instead an object and the signal name. class SignalAssertParameters: var object = null var signal_name = null var sig = null var others := [] func _init(p1, p2, p3=null, p4=null, p5=null, p6=null): others = [p3, p4, p5, p6] if(p1 is Signal): object = p1.get_object() signal_name = p1.get_name() others.push_front(p2) sig = p1 else: object = p1 signal_name = p2 sig = object.get(signal_name) const EDITOR_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE | PROPERTY_USAGE_DEFAULT const VARIABLE_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE # Convenience copy of GutUtils.DOUBLE_STRATEGY var DOUBLE_STRATEGY = GutUtils.DOUBLE_STRATEGY ## Reference to [addons/gut/parameter_factory.gd] script. var ParameterFactory = GutUtils.ParameterFactory ## @ignore var CompareResult = GutUtils.CompareResult ## Reference to [GutInputFactory] class that was originally used to reference ## the Input Factory before the class_name was introduced. var InputFactory = GutInputFactory ## Reference to [GutInputSender]. This was the way you got to the [GutInputSender] ## before it was given a [code]class_name[/code] var InputSender = GutUtils.InputSender # Need a reference to the instance that is running the tests. This # is set by the gut class when it runs the test script. var gut: GutMain = null # Reference to the collected_script.gd instance that was used to create this. # This makes getting to meta data about the test easier. This is set by # collected_script.get_new(). var collected_script = null var wait_log_delay = .5 : set(val): if(_awaiter != null): _awaiter.await_logger.wait_log_delay = val wait_log_delay = val var _compare = GutUtils.Comparator.new() var _disable_strict_datatype_checks = false # Holds all the text for a test's fail/pass. This is used for testing purposes # to see the text of a failed sub-test in test_test.gd var _fail_pass_text = [] # Summary counts for the test. var _summary = { asserts = 0, passed = 0, failed = 0, tests = 0, pending = 0 } # This is used to watch signals so we can make assertions about them. var _signal_watcher = load('res://addons/gut/signal_watcher.gd').new() var _lgr = GutUtils.get_logger() var _strutils = GutUtils.Strutils.new() var _awaiter = null var _was_ready_called = false # I haven't decided if we should be using _ready or not. Right now gut.gd will # call this if _ready was not called (because it was overridden without a super # call). Maybe gut.gd should just call _do_ready_stuff (after we rename it to # something better). I'm leaving all this as it is until it bothers me more. func _do_ready_stuff(): _awaiter = GutUtils.Awaiter.new() _awaiter.await_logger.wait_log_delay = wait_log_delay add_child(_awaiter) _was_ready_called = true func _ready(): _do_ready_stuff() func _notification(what): # Tests are never expected to re-enter the tree. Tests are removed from the # tree after they are run. if(what == NOTIFICATION_EXIT_TREE): # print(_strutils.type2str(self), ': exit_tree') _awaiter.queue_free() elif(what == NOTIFICATION_PREDELETE): # print(_strutils.type2str(self), ': predelete') if(is_instance_valid(_awaiter)): _awaiter.queue_free() #region Private # ---------------- func _str(thing): return _strutils.type2str(thing) func _str_precision(value, precision): var to_return = _str(value) var format = str('%.', precision, 'f') if(typeof(value) == TYPE_FLOAT): to_return = format % value elif(typeof(value) == TYPE_VECTOR2): to_return = str('VECTOR2(', format % value.x, ', ', format %value.y, ')') elif(typeof(value) == TYPE_VECTOR3): to_return = str('VECTOR3(', format % value.x, ', ', format %value.y, ', ', format % value.z, ')') return to_return # Fail an assertion. Causes test and script to fail as well. func _fail(text): _summary.asserts += 1 _summary.failed += 1 _fail_pass_text.append('failed: ' + text) if(gut): _lgr.failed(gut.get_call_count_text() + text) gut._fail(text) # Pass an assertion. func _pass(text): _summary.asserts += 1 _summary.passed += 1 _fail_pass_text.append('passed: ' + text) if(gut): _lgr.passed(text) gut._pass(text) # Checks if the datatypes passed in match. If they do not then this will cause # a fail to occur. If they match then TRUE is returned, FALSE if not. This is # used in all the assertions that compare values. func _do_datatypes_match__fail_if_not(got, expected, text): var did_pass = true if(!_disable_strict_datatype_checks): var got_type = typeof(got) var expect_type = typeof(expected) if(got_type != expect_type and got != null and expected != null): # If we have a mismatch between float and int (types 2 and 3) then # print out a warning but do not fail. if([2, 3].has(got_type) and [2, 3].has(expect_type)): _lgr.warn(str('Warn: Float/Int comparison. Got ', _strutils.types[got_type], ' but expected ', _strutils.types[expect_type])) elif([TYPE_STRING, TYPE_STRING_NAME].has(got_type) and [TYPE_STRING, TYPE_STRING_NAME].has(expect_type)): pass else: _fail('Cannot compare ' + _strutils.types[got_type] + '[' + _str(got) + '] to ' + \ _strutils.types[expect_type] + '[' + _str(expected) + ']. ' + text) did_pass = false return did_pass # Create a string that lists all the methods that were called on an spied # instance. func _get_desc_of_calls_to_instance(inst): var BULLET = ' * ' var calls = gut.get_spy().get_call_list_as_string(inst) # indent all the calls calls = BULLET + calls.replace("\n", "\n" + BULLET) # remove_at trailing newline and bullet calls = calls.substr(0, calls.length() - BULLET.length() - 1) return "Calls made on " + str(inst) + "\n" + calls # Signal assertion helper. Do not call directly, use _can_make_signal_assertions func _fail_if_does_not_have_signal(object, signal_name): var did_fail = false if(!_signal_watcher.does_object_have_signal(object, signal_name)): _fail(str('Object ', object, ' does not have the signal [', signal_name, ']')) did_fail = true return did_fail # Signal assertion helper. Do not call directly, use _can_make_signal_assertions func _fail_if_not_watching(object): var did_fail = false if(!_signal_watcher.is_watching_object(object)): _fail(str('Cannot make signal assertions because the object ', object, \ ' is not being watched. Call watch_signals(some_object) to be able to make assertions about signals.')) did_fail = true return did_fail # Returns text that contains original text and a list of all the signals that # were emitted for the passed in object. func _get_fail_msg_including_emitted_signals(text, object): return str(text," (Signals emitted: ", _signal_watcher.get_signals_emitted(object), ")") # This validates that parameters is an array and generates a specific error # and a failure with a specific message func _fail_if_parameters_not_array(parameters): var invalid = parameters != null and typeof(parameters) != TYPE_ARRAY if(invalid): _lgr.error('The "parameters" parameter must be an array of expected parameter values.') _fail('Cannot compare parameter values because an array was not passed.') return invalid # A bunch of common checkes used when validating a double/method pair. If # everything is ok then an empty string is returned, otherwise the message # is returned. func _get_bad_method_message(inst, method_name, what_you_cant_do): var to_return = '' if(!inst.has_method(method_name)): to_return = str("You cannot ", what_you_cant_do, " [", method_name, "] because the method does not exist. ", "This can happen if the method is virtual and not overloaded (i.e. _ready) ", "or you have mistyped the name of the method.") elif(!inst.__gutdbl_values.doubled_methods.has(method_name)): to_return = str("You cannot ", what_you_cant_do, " [", method_name, "] because ", _str(inst), ' does not overload it or it was ignored with ', 'ignore_method_when_doubling. See Doubling ', 'Strategy in the wiki for details on including non-overloaded ', 'methods in a double.') return to_return func _fail_if_not_double_or_does_not_have_method(inst, method_name): var to_return = OK if(!GutUtils.is_double(inst)): _fail(str("An instance of a Double was expected, you passed: ", _str(inst))) to_return = ERR_INVALID_DATA else: var msg = _get_bad_method_message(inst, method_name, 'spy on') if(msg != ''): _fail(msg) to_return = ERR_INVALID_DATA return to_return func _create_obj_from_type(type): var obj = null if type.is_class("PackedScene"): obj = type.instantiate() add_child(obj) else: obj = type.new() return obj # Converts a Callabe passed through inst or inst/method_name/parameters into a # hash so that methods that interact with Spy can accept both more easily. func _convert_spy_args(inst, method_name, parameters): var to_return = { 'object':inst, 'method_name':method_name, 'arguments':parameters, 'invalid_message':'ok' } if(inst is Callable): if(parameters != null): to_return.invalid_message =\ "3rd parameter to assert_called not supported when using a Callable." elif(method_name != null): to_return.invalid_message =\ "2nd parameter to assert_called not supported when using a Callable." else: if(inst.get_bound_arguments_count() > 0): to_return.arguments = inst.get_bound_arguments() to_return.method_name = inst.get_method() to_return.object = inst.get_object() return to_return func _get_typeof_string(the_type): var to_return = "" if(_strutils.types.has(the_type)): to_return += str(the_type, '(', _strutils.types[the_type], ')') else: to_return += str(the_type) return to_return # Validates the singleton_name is a string and exists. Errors when conditions # are not met. Returns true/false if singleton_name is valid or not. func _validate_singleton_name(singleton_name): var is_valid = true if(typeof(singleton_name) != TYPE_STRING): _lgr.error("double_singleton requires a Godot singleton name, you passed " + _str(singleton_name)) is_valid = false # Sometimes they have underscores in front of them, sometimes they do not. # The doubler is smart enought of ind the right thing, so this has to be # that smart as well. elif(!ClassDB.class_exists(singleton_name) and !ClassDB.class_exists('_' + singleton_name)): var txt = str("The singleton [", singleton_name, "] could not be found. ", "Check the GlobalScope page for a list of singletons.") _lgr.error(txt) is_valid = false return is_valid # Checks the object for 'get_' and 'set_' methods for the specified property. # If found a warning is generated. func _warn_for_public_accessors(obj, property_name): var public_accessors = [] var accessor_names = [ str('get_', property_name), str('is_', property_name), str('set_', property_name) ] for acc in accessor_names: if(obj.has_method(acc)): public_accessors.append(acc) if(public_accessors.size() > 0): _lgr.warn (str('Public accessors ', public_accessors, ' found for property ', property_name)) func _smart_double(thing, double_strat, partial): var override_strat = GutUtils.nvl(double_strat, gut.get_doubler().get_strategy()) var to_return = null if(thing is PackedScene): if(partial): to_return = gut.get_doubler().partial_double_scene(thing, override_strat) else: to_return = gut.get_doubler().double_scene(thing, override_strat) elif(GutUtils.is_native_class(thing)): if(partial): to_return = gut.get_doubler().partial_double_gdnative(thing) else: to_return = gut.get_doubler().double_gdnative(thing) elif(thing is GDScript): if(partial): to_return = gut.get_doubler().partial_double(thing, override_strat) else: to_return = gut.get_doubler().double(thing, override_strat) return to_return # This is here to aid in the transition to the new doubling sytnax. Once this # has been established it could be removed. We must keep the is_instance check # going forward though. func _are_double_parameters_valid(thing, p2, p3): var bad_msg = "" if(p3 != null or typeof(p2) == TYPE_STRING): bad_msg += "Doubling using a subpath is not supported. Call register_inner_class and then pass the Inner Class to double().\n" if(typeof(thing) == TYPE_STRING): bad_msg += "Doubling using the path to a script or scene is no longer supported. Load the script or scene and pass that to double instead.\n" if(GutUtils.is_instance(thing)): bad_msg += "double requires a script, you passed an instance: " + _str(thing) if(bad_msg != ""): _lgr.error(bad_msg) return bad_msg == "" # ---------------- #endregion #region Virtual Methods # ---------------- ## Virtual Method. This is run after the script has been prepped for execution, but before `before_all` is executed. If you implement this method and return `true` or a `String` (the string is displayed in the log) then GUT will stop executing the script and mark it as risky. You might want to do this because: ## - You are porting tests from 3.x to 4.x and you don't want to comment everything out.[br] ## - Skipping tests that should not be run when in `headless` mode such as input testing that does not work in headless.[br] ## [codeblock] ## func should_skip_script(): ## if DisplayServer.get_name() == "headless": ## return "Skip Input tests when running headless" ## [/codeblock] ## - If you have tests that would normally cause the debugger to break on an error, you can skip the script if the debugger is enabled so that the run is not interrupted.[br] ## [codeblock] ## func should_skip_script(): ## return EngineDebugger.is_active() ## [/codeblock] func should_skip_script(): return false ## Virtual method. Run once before anything else in the test script is run. func before_all(): pass ## Virtual method. Run before each test is executed func before_each(): pass ## Virtual method. Run after each test is executed. func after_each(): pass ## Virtual method. Run after all tests have been run. func after_all(): pass # ---------------- #endregion #region Misc Public # ---------------- ## Mark the current test as pending. func pending(text=""): _summary.pending += 1 if(gut): _lgr.pending(text) gut._pending(text) ## Returns true if the test is passing as of the time of this call. False if not. func is_passing(): if(gut.get_current_test_object() != null and !['before_all', 'after_all'].has(gut.get_current_test_object().name)): return gut.get_current_test_object().is_passing() and \ gut.get_current_test_object().assert_count > 0 else: _lgr.error('No current test object found. is_passing must be called inside a test.') return null ## Returns true if the test is failing as of the time of this call. False if not. func is_failing(): if(gut.get_current_test_object() != null and !['before_all', 'after_all'].has(gut.get_current_test_object().name)): return gut.get_current_test_object().is_failing() else: _lgr.error('No current test object found. is_failing must be called inside a test.') return null ## Marks the test as passing. Does not override any failing asserts or calls to ## fail_test. Same as a passing assert. func pass_test(text): _pass(text) ## Marks the test as failing. Same as a failing assert. func fail_test(text): _fail(text) ## @internal func clear_signal_watcher(): _signal_watcher.clear() ## Returns the current double strategy. func get_double_strategy(): return gut.get_doubler().get_strategy() ## Sets the double strategy for all tests in the script. This should usually ## be done in [method before_all]. The double strtegy can be set per ## run/script/double. See [wiki]Double-Strategy[/wiki] func set_double_strategy(double_strategy): gut.get_doubler().set_strategy(double_strategy) ## This method will cause Gut to pause before it moves on to the next test. ## This is useful for debugging, for instance if you want to investigate the ## screen or anything else after a test has finished executing. ## [br] ## Sometimes you get lazy, and you don't remove calls to ## [code skip-lint]pause_before_teardown[/code] after you are done with them. You can ## tell GUT to ignore calls to this method through the panel or ## the command line. Setting this in your `.gutconfig.json` file is recommended ## for CI/CD Pipelines. func pause_before_teardown(): gut.pause_before_teardown() ## @internal func get_logger(): return _lgr ## @internal func set_logger(logger): _lgr = logger ## This must be called in order to make assertions based on signals being ## emitted. __Right now, this only supports signals that are emitted with 9 or ## less parameters.__ This can be extended but nine seemed like enough for now. ## The Godot documentation suggests that the limit is four but in my testing ## I found you can pass more. ## [br] ## This must be called in each test in which you want to make signal based ## assertions in. You can call it multiple times with different objects. ## You should not call it multiple times with the same object in the same test. ## The objects that are watched are cleared after each test (specifically right ## before `teardown` is called). Under the covers, Gut will connect to all the ## signals an object has and it will track each time they fire. You can then ## use the following asserts and methods to verify things are acting correct. func watch_signals(object): _signal_watcher.watch_signals(object) ## This will return the number of times a signal was fired. This gives you ## the freedom to make more complicated assertions if the spirit moves you. ## This will return -1 if the signal was not fired or the object was not being ## watched, or if the object does not have the signal. ## [br][br] ## Accepts either the object and the signal name or the signal. func get_signal_emit_count(p1, p2=null): var sp = SignalAssertParameters.new(p1, p2) return _signal_watcher.get_emit_count(sp.object, sp.signal_name) ## If you need to inspect the parameters in order to make more complicate assertions, then this will give you access to ## the parameters of any watched signal. This works the same way that ## [code skip-lint]assert_signal_emitted_with_parameters[/code] does. It takes an object, signal name, and an optional ## index. If the index is not specified then the parameters from the most recent emission will be returned. If the ## object is not being watched, the signal was not fired, or the object does not have the signal then `null` will be ## returned. ## ## [br][br] ## [b]Signatures:[/b][br] ## - get_signal_parameters([param p1]:Signal, [param p2]:parameter-index (optional))[br] ## - get_signal_parameters([param p1]:object, [param p2]:signal name, [param p3]:parameter-index (optional)) [br] ## [br] ## [b]Examples:[/b] ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## ## func test_get_signal_parameters(): ## var obj = SignalObject.new() ## watch_signals(obj) ## obj.some_signal.emit(1, 2, 3) ## obj.some_signal.emit('a', 'b', 'c') ## ## # -- Passing -- ## # passes because get_signal_parameters returns the most recent emission ## # by default ## assert_eq(get_signal_parameters(obj, 'some_signal'), ['a', 'b', 'c']) ## assert_eq(get_signal_parameters(obj.some_signal), ['a', 'b', 'c']) ## ## assert_eq(get_signal_parameters(obj, 'some_signal', 0), [1, 2, 3]) ## assert_eq(get_signal_parameters(obj.some_signal, 0), [1, 2, 3]) ## ## # if the signal was not fired null is returned ## assert_null(get_signal_parameters(obj, 'other_signal')) ## # if the signal does not exist or isn't being watched null is returned ## assert_null(get_signal_parameters(obj, 'signal_dne')) ## ## # -- Failing -- ## assert_eq(get_signal_parameters(obj, 'some_signal'), [1, 2, 3]) ## assert_eq(get_signal_parameters(obj.some_signal, 0), ['a', 'b', 'c']) ## [/codeblock] func get_signal_parameters(p1, p2=null, p3=-1): var sp := SignalAssertParameters.new(p1, GutUtils.nvl(p2, -1), p3) return _signal_watcher.get_signal_parameters(sp.object, sp.signal_name, sp.others[0]) ## Get the parameters for a method call to a doubled object. By default it will ## return the most recent call. You can optionally specify an index for which ## call you want to get the parameters for. ## ## Can be called using a Callable for the first parameter instead of specifying ## an object and method name. When you do this, the seoncd parameter is used ## as the index. ## ## Returns: ## * an array of parameter values if a call the method was found ## * null when a call to the method was not found or the index specified was ## invalid. func get_call_parameters(object, method_name_or_index = -1, idx=-1): var to_return = null var index = idx if(object is Callable): index = method_name_or_index method_name_or_index = null var converted = _convert_spy_args(object, method_name_or_index, null) if(GutUtils.is_double(converted.object)): to_return = gut.get_spy().get_call_parameters( converted.object, converted.method_name, index) else: _lgr.error('You must pass a doulbed object to get_call_parameters.') return to_return ## Returns the call count for a method with optional paramter matching. ## ## Can be called with a Callable instead of an object, method_name, and ## parameters. Bound arguments will be used to match call arguments. func get_call_count(object, method_name=null, parameters=null): var converted = _convert_spy_args(object, method_name, parameters) return gut.get_spy().call_count(converted.object, converted.method_name, converted.arguments) ## Simulate a number of frames by calling '_process' and '_physics_process' (if ## the methods exist) on an object and all of its descendents. The specified frame ## time, 'delta', will be passed to each simulated call. ## ## NOTE: Objects can disable their processing methods using 'set_process(false)' and ## 'set_physics_process(false)'. This is reflected in the 'Object' methods ## 'is_processing()' and 'is_physics_processing()', respectively. To make 'simulate' ## respect this status, for example if you are testing an object which toggles ## processing, pass 'check_is_processing' as 'true'. func simulate(obj, times, delta, check_is_processing: bool = false): gut.simulate(obj, times, delta, check_is_processing) # ------------------------------------------------------------------------------ ## Replace the node at base_node.get_node(path) with with_this. All references ## to the node via $ and get_node(...) will now return with_this. with_this will ## get all the groups that the node that was replaced had. ## [br] ## The node that was replaced is queued to be freed. ## [br] ## TODO see replace_by method, this could simplify the logic here. # ------------------------------------------------------------------------------ func replace_node(base_node, path_or_node, with_this): var path = path_or_node if(typeof(path_or_node) != TYPE_STRING): # This will cause an engine error if it fails. It always returns a # NodePath, even if it fails. Checking the name count is the only way # I found to check if it found something or not (after it worked I # didn't look any farther). path = base_node.get_path_to(path_or_node) if(path.get_name_count() == 0): _lgr.error('You passed an object that base_node does not have. Cannot replace node.') return if(!base_node.has_node(path)): _lgr.error(str('Could not find node at path [', path, ']')) return var to_replace = base_node.get_node(path) var parent = to_replace.get_parent() var replace_name = to_replace.get_name() parent.remove_child(to_replace) parent.add_child(with_this) with_this.set_name(replace_name) with_this.set_owner(parent) var groups = to_replace.get_groups() for i in range(groups.size()): with_this.add_to_group(groups[i]) to_replace.queue_free() ## Use this as the default value for the first parameter to a test to create ## a parameterized test. See also the ParameterFactory and Parameterized Tests. ## [br][br] ## [b]Example[/b] ## [codeblock] ## func test_with_parameters(p = use_parameters([1, 2, 3])): ## [/codeblock] func use_parameters(params): var ph = gut.parameter_handler if(ph == null): ph = GutUtils.ParameterHandler.new(params) gut.parameter_handler = ph # DO NOT use gut.gd's get_call_count_text here since it decrements the # get_call_count value. This method increments the call count in its # return statement. var output = str('- params[', ph.get_call_count(), ']','(', ph.get_current_parameters(), ')') gut.p(output, gut.LOG_LEVEL_TEST_AND_FAILURES) return ph.next_parameters() ## @internal ## When used as the default for a test method parameter, it will cause the test ## to be run x times. ## ## I Hacked this together to test a method that was occassionally failing due to ## timing issues. I don't think it's a great idea, but you be the judge. If ## you find a good use for it, let me know and I'll make it a legit member ## of the api. func run_x_times(x): var ph = gut.parameter_handler if(ph == null): _lgr.warn( str("This test uses run_x_times and you really should not be ", "using it. I don't think it's a good thing, but I did find it ", "temporarily useful so I left it in here and didn't document it. ", "Well, you found it, might as well open up an issue and let me ", "know why you're doing this.")) var params = [] for i in range(x): params.append(i) ph = GutUtils.ParameterHandler.new(params) gut.parameter_handler = ph return ph.next_parameters() ## Checks the passed in version string (x.x.x) against the engine version to see ## if the engine version is less than the expected version. If it is then the ## test is mareked as passed (for a lack of anything better to do). The result ## of the check is returned. ## [br][br] ## [b]Example[/b] ## [codeblock] ## if(skip_if_godot_version_lt('3.5.0')): ## return ## [/codeblock] func skip_if_godot_version_lt(expected): var should_skip = !GutUtils.is_godot_version_gte(expected) if(should_skip): _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is less than ', expected)) return should_skip ## Checks if the passed in version matches the engine version. The passed in ## version can contain just the major, major.minor or major.minor.path. If ## the version is not the same then the test is marked as passed. The result of ## the check is returned. ## [br][br] ## [b]Example[/b] ## [codeblock] ## if(skip_if_godot_version_ne('3.4')): ## return ## [/codeblock] func skip_if_godot_version_ne(expected): var should_skip = !GutUtils.is_godot_version(expected) if(should_skip): _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is not ', expected)) return should_skip ## Registers all the inner classes in a script with the doubler. This is required ## before you can double any inner class. func register_inner_classes(base_script): gut.get_doubler().inner_class_registry.register(base_script) ## Peforms a deep compare on both values, a CompareResult instnace is returned. ## The optional max_differences paramter sets the max_differences to be displayed. func compare_deep(v1, v2, max_differences=null): var result = _compare.deep(v1, v2) if(max_differences != null): result.max_differences = max_differences return result # ---------------- #endregion #region Asserts # ---------------- ## Asserts that the expected value equals the value got. ## assert got == expected and prints optional text. See [wiki]Comparing-Things[/wiki] ## for information about comparing dictionaries and arrays. ## [br] ## See also: [method assert_ne], [method assert_same], [method assert_not_same] ## [codeblock] ## var one = 1 ## var node1 = Node.new() ## var node2 = node1 ## ## # Passing ## assert_eq(one, 1, 'one should equal one') ## assert_eq('racecar', 'racecar') ## assert_eq(node2, node1) ## assert_eq([1, 2, 3], [1, 2, 3]) ## var d1_pass = {'a':1} ## var d2_pass = d1_pass ## assert_eq(d1_pass, d2_pass) ## ## # Failing ## assert_eq(1, 2) # FAIL ## assert_eq('hello', 'world') ## assert_eq(self, node1) ## assert_eq([1, 'two', 3], [1, 2, 3, 4]) ## assert_eq({'a':1}, {'a':1}) ## [/codeblock] func assert_eq(got, expected, text=""): if(_do_datatypes_match__fail_if_not(got, expected, text)): var disp = "[" + _str(got) + "] expected to equal [" + _str(expected) + "]: " + text var result = null result = _compare.simple(got, expected) if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): disp = str(result.summary, ' ', text) _lgr.info('Array/Dictionary compared by value. Use assert_same to compare references. Use assert_eq_deep to see diff when failing.') if(result.are_equal): _pass(disp) else: _fail(disp) ## asserts got != expected and prints optional text. See ## [wiki]Comparing-Things[/wiki] for information about comparing dictionaries ## and arrays. ##[br] ## See also: [method assert_eq], [method assert_same], [method assert_not_same] ## [codeblock] ## var two = 2 ## var node1 = Node.new() ## ## # Passing ## assert_ne(two, 1, 'Two should not equal one.') ## assert_ne('hello', 'world') ## assert_ne(self, node1) ## ## # Failing ## assert_ne(two, 2) ## assert_ne('one', 'one') ## assert_ne('2', 2) ## [/codeblock] func assert_ne(got, not_expected, text=""): if(_do_datatypes_match__fail_if_not(got, not_expected, text)): var disp = "[" + _str(got) + "] expected to not equal [" + _str(not_expected) + "]: " + text var result = null result = _compare.simple(got, not_expected) if(typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]): disp = str(result.summary, ' ', text) _lgr.info('Array/Dictionary compared by value. Use assert_not_same to compare references. Use assert_ne_deep to see diff.') if(result.are_equal): _fail(disp) else: _pass(disp) ## Asserts that [param got] is within the range of [param expected] +/- [param error_interval]. ## The upper and lower bounds are included in the check. Verified to work with ## integers, floats, and Vector2. Should work with anything that can be ## added/subtracted. ## ## [codeblock] ## # Passing ## assert_almost_eq(0, 1, 1, '0 within range of 1 +/- 1') ## assert_almost_eq(2, 1, 1, '2 within range of 1 +/- 1') ## assert_almost_eq(1.2, 1.0, .5, '1.2 within range of 1 +/- .5') ## assert_almost_eq(.5, 1.0, .5, '.5 within range of 1 +/- .5') ## assert_almost_eq(Vector2(.5, 1.5), Vector2(1.0, 1.0), Vector2(.5, .5)) ## assert_almost_eq(Vector2(.5, 1.5), Vector2(1.0, 1.0), Vector2(.25, .25)) ## ## # Failing ## assert_almost_eq(1, 3, 1, '1 outside range of 3 +/- 1') ## assert_almost_eq(2.6, 3.0, .2, '2.6 outside range of 3 +/- .2') ## [/codeblock] func assert_almost_eq(got, expected, error_interval, text=''): var disp = "[" + _str_precision(got, 20) + "] expected to equal [" + _str(expected) + "] +/- [" + str(error_interval) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): if not _is_almost_eq(got, expected, error_interval): _fail(disp) else: _pass(disp) ## This is the inverse of [method assert_almost_eq]. This will pass if [param got] is ## outside the range of [param not_expected] +/- [param error_interval]. func assert_almost_ne(got, not_expected, error_interval, text=''): var disp = "[" + _str_precision(got, 20) + "] expected to not equal [" + _str(not_expected) + "] +/- [" + str(error_interval) + "]: " + text if(_do_datatypes_match__fail_if_not(got, not_expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)): if _is_almost_eq(got, not_expected, error_interval): _fail(disp) else: _pass(disp) # ------------------------------------------------------------------------------ # Helper function compares a value against a expected and a +/- range. Compares # all components of Vector2, Vector3, and Vector4 as well. # ------------------------------------------------------------------------------ func _is_almost_eq(got, expected, error_interval) -> bool: var result = false var upper = expected + error_interval var lower = expected - error_interval if typeof(got) in [TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4]: result = got.clamp(lower, upper) == got else: result = got >= (lower) and got <= (upper) return(result) ## assserts got > expected ## [codeblock] ## var bigger = 5 ## var smaller = 0 ## ## # Passing ## assert_gt(bigger, smaller, 'Bigger should be greater than smaller') ## assert_gt('b', 'a') ## assert_gt('a', 'A') ## assert_gt(1.1, 1) ## ## # Failing ## assert_gt('a', 'a') ## assert_gt(1.0, 1) ## assert_gt(smaller, bigger) ## [/codeblock] func assert_gt(got, expected, text=""): var disp = "[" + _str(got) + "] expected to be > than [" + _str(expected) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expected, text)): if(got > expected): _pass(disp) else: _fail(disp) ## Asserts got is greater than or equal to expected. ## [codeblock] ## var bigger = 5 ## var smaller = 0 ## ## # Passing ## assert_gte(bigger, smaller, 'Bigger should be greater than or equal to smaller') ## assert_gte('b', 'a') ## assert_gte('a', 'A') ## assert_gte(1.1, 1) ## assert_gte('a', 'a') ## ## # Failing ## assert_gte(0.9, 1.0) ## assert_gte(smaller, bigger) ## [/codeblock] func assert_gte(got, expected, text=""): var disp = "[" + _str(got) + "] expected to be >= than [" + _str(expected) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expected, text)): if(got >= expected): _pass(disp) else: _fail(disp) ## Asserts [param got] is less than [param expected] ## [codeblock] ## var bigger = 5 ## var smaller = 0 ## ## # Passing ## assert_lt(smaller, bigger, 'Smaller should be less than bigger') ## assert_lt('a', 'b') ## assert_lt(99, 100) ## ## # Failing ## assert_lt('z', 'x') ## assert_lt(-5, -5) ## [/codeblock] func assert_lt(got, expected, text=""): var disp = "[" + _str(got) + "] expected to be < than [" + _str(expected) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expected, text)): if(got < expected): _pass(disp) else: _fail(disp) ## Asserts got is less than or equal to expected func assert_lte(got, expected, text=""): var disp = "[" + _str(got) + "] expected to be <= than [" + _str(expected) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expected, text)): if(got <= expected): _pass(disp) else: _fail(disp) ## asserts that got is true. Does not assert truthiness, only boolean values ## will pass. func assert_true(got, text=""): if(typeof(got) == TYPE_BOOL): if(got): _pass(text) else: _fail(text) else: var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") _fail(msg) ## Asserts that got is false. Does not assert truthiness, only boolean values ## will pass. func assert_false(got, text=""): if(typeof(got) == TYPE_BOOL): if(got): _fail(text) else: _pass(text) else: var msg = str("Cannot convert ", _strutils.type2str(got), " to boolean") _fail(msg) ## Asserts value is between (inclusive) the two expected values.[br] ## got >= expect_low and <= expect_high ## [codeblock] ## # Passing ## assert_between(5, 0, 10, 'Five should be between 0 and 10') ## assert_between(10, 0, 10) ## assert_between(0, 0, 10) ## assert_between(2.25, 2, 4.0) ## ## # Failing ## assert_between('a', 'b', 'c') ## assert_between(1, 5, 10) ## [/codeblock] func assert_between(got, expect_low, expect_high, text=""): var disp = "[" + _str_precision(got, 20) + "] expected to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): if(expect_low > expect_high): disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" _fail(disp) else: if(got < expect_low or got > expect_high): _fail(disp) else: _pass(disp) ## Asserts value is not between (exclusive) the two expected values.[br] ## asserts that got <= expect_low or got >= expect_high. ## [codeblock] ## # Passing ## assert_not_between(1, 5, 10) ## assert_not_between('a', 'b', 'd') ## assert_not_between('d', 'b', 'd') ## assert_not_between(10, 0, 10) ## assert_not_between(-2, -2, 10) ## ## # Failing ## assert_not_between(5, 0, 10, 'Five shouldnt be between 0 and 10') ## assert_not_between(0.25, -2.0, 4.0) ## [/codeblock] func assert_not_between(got, expect_low, expect_high, text=""): var disp = "[" + _str_precision(got, 20) + "] expected not to be between [" + _str(expect_low) + "] and [" + str(expect_high) + "]: " + text if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)): if(expect_low > expect_high): disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]" _fail(disp) else: if(got > expect_low and got < expect_high): _fail(disp) else: _pass(disp) ## Uses the 'has' method of the object passed in to determine if it contains ## the passed in element. ## [codeblock] ## var an_array = [1, 2, 3, 'four', 'five'] ## var a_hash = { 'one':1, 'two':2, '3':'three'} ## ## # Passing ## assert_has(an_array, 'four') # PASS ## assert_has(an_array, 2) # PASS ## # the hash's has method checks indexes not values ## assert_has(a_hash, 'one') # PASS ## assert_has(a_hash, '3') # PASS ## ## # Failing ## assert_has(an_array, 5) # FAIL ## assert_has(an_array, self) # FAIL ## assert_has(a_hash, 3) # FAIL ## assert_has(a_hash, 'three') # FAIL ## [/codeblock] func assert_has(obj, element, text=""): var disp = str('Expected [', _str(obj), '] to contain value: [', _str(element), ']: ', text) if(obj.has(element)): _pass(disp) else: _fail(disp) ## The inverse of assert_has. func assert_does_not_have(obj, element, text=""): var disp = str('Expected [', _str(obj), '] to NOT contain value: [', _str(element), ']: ', text) if(obj.has(element)): _fail(disp) else: _pass(disp) ## asserts a file exists at the specified path ## [codeblock] ## func before_each(): ## gut.file_touch('user://some_test_file') ## ## func after_each(): ## gut.file_delete('user://some_test_file') ## ## func test_assert_file_exists(): ## # Passing ## assert_file_exists('res://addons/gut/gut.gd') ## assert_file_exists('user://some_test_file') ## ## # Failing ## assert_file_exists('user://file_does_not.exist') ## assert_file_exists('res://some_dir/another_dir/file_does_not.exist') ## [/codeblock] func assert_file_exists(file_path): var disp = 'expected [' + file_path + '] to exist.' if(FileAccess.file_exists(file_path)): _pass(disp) else: _fail(disp) ## asserts a file does not exist at the specified path ## [codeblock] ## func before_each(): ## gut.file_touch('user://some_test_file') ## ## func after_each(): ## gut.file_delete('user://some_test_file') ## ## func test_assert_file_does_not_exist(): ## # Passing ## assert_file_does_not_exist('user://file_does_not.exist') ## assert_file_does_not_exist('res://some_dir/another_dir/file_does_not.exist') ## ## # Failing ## assert_file_does_not_exist('res://addons/gut/gut.gd') ## [/codeblock] func assert_file_does_not_exist(file_path): var disp = 'expected [' + file_path + '] to NOT exist' if(!FileAccess.file_exists(file_path)): _pass(disp) else: _fail(disp) ## asserts the specified file is empty ## [codeblock] ## func before_each(): ## gut.file_touch('user://some_test_file') ## ## func after_each(): ## gut.file_delete('user://some_test_file') ## ## func test_assert_file_empty(): ## # Passing ## assert_file_empty('user://some_test_file') ## ## # Failing ## assert_file_empty('res://addons/gut/gut.gd') ## [/codeblock] func assert_file_empty(file_path): var disp = 'expected [' + file_path + '] to be empty' if(FileAccess.file_exists(file_path) and gut.is_file_empty(file_path)): _pass(disp) else: _fail(disp) ## Asserts the specified file is not empty ## [codeblock] ## func before_each(): ## gut.file_touch('user://some_test_file') ## ## func after_each(): ## gut.file_delete('user://some_test_file') ## ## func test_assert_file_not_empty(): ## # Passing ## assert_file_not_empty('res://addons/gut/gut.gd') # PASS ## ## # Failing ## assert_file_not_empty('user://some_test_file') # FAIL ## [/codeblock] func assert_file_not_empty(file_path): var disp = 'expected [' + file_path + '] to contain data' if(!gut.is_file_empty(file_path)): _pass(disp) else: _fail(disp) ## Asserts that the passed in object has a method named [param method]. func assert_has_method(obj, method, text=''): var disp = _str(obj) + ' should have method: ' + method if(text != ''): disp = _str(obj) + ' ' + text assert_true(obj.has_method(method), disp) ## This is meant to make testing public get/set methods for a member variable. This was originally created for early Godot 3.x setter and getter methods. See [method assert_property] for verifying Godot 4.x accessors. This makes multiple assertions to verify: ## [br] ## [li]The object has a method called [code]get_[/code][/li] ## [li]The object has a method called [code]set_[/code][/li] ## [li]The method [code]get_[/code] returns the expected default value when first called.[/li] ## [li]Once you set the property, the [code]get_[/code] returns the new value.[/li] ## [br] func assert_accessors(obj, property, default, set_to): var fail_count = _summary.failed var get_func = 'get_' + property var set_func = 'set_' + property if(obj.has_method('is_' + property)): get_func = 'is_' + property assert_has_method(obj, get_func, 'should have getter starting with get_ or is_') assert_has_method(obj, set_func) # SHORT CIRCUIT if(_summary.failed > fail_count): return assert_eq(obj.call(get_func), default, 'It should have the expected default value.') obj.call(set_func, set_to) assert_eq(obj.call(get_func), set_to, 'The set value should have been returned.') # Property search helper. Used to retrieve Dictionary of specified property # from passed object. Returns null if not found. # If provided, property_usage constrains the type of property returned by # passing either: # EDITOR_PROPERTY for properties defined as: export var some_value: int # VARIABLE_PROPERTY for properties defined as: var another_value func _find_object_property(obj, property_name, property_usage=null): var result = null var found = false var properties = obj.get_property_list() while !found and !properties.is_empty(): var property = properties.pop_back() if property['name'] == property_name: if property_usage == null or property['usage'] == property_usage: result = property found = true return result ## Asserts that [param obj] exports a property with the name ## [param property_name] and a type of [param type]. The [param type] must be ## one of the various Godot built-in [code]TYPE_[/code] constants. ## [codeblock] ## class ExportClass: ## export var some_number = 5 ## export(PackedScene) var some_scene ## var some_variable = 1 ## ## func test_assert_exports(): ## var obj = ExportClass.new() ## ## # Passing ## assert_exports(obj, "some_number", TYPE_INT) ## assert_exports(obj, "some_scene", TYPE_OBJECT) ## ## # Failing ## assert_exports(obj, 'some_number', TYPE_VECTOR2) ## assert_exports(obj, 'some_scene', TYPE_AABB) ## assert_exports(obj, 'some_variable', TYPE_INT) ## [/codeblock] func assert_exports(obj, property_name, type): var disp = 'expected %s to have editor property [%s]' % [_str(obj), property_name] var property = _find_object_property(obj, property_name, EDITOR_PROPERTY) if property != null: disp += ' of type [%s]. Got type [%s].' % [_strutils.types[type], _strutils.types[property['type']]] if property['type'] == type: _pass(disp) else: _fail(disp) else: _fail(disp) # Signal assertion helper. # # Verifies that the object and signal are valid for making signal assertions. # This will fail with specific messages that indicate why they are not valid. # This returns true/false to indicate if the object and signal are valid. func _can_make_signal_assertions(object, signal_name): return !(_fail_if_not_watching(object) or _fail_if_does_not_have_signal(object, signal_name)) # Check if an object is connected to a signal on another object. Returns True # if it is and false otherwise func _is_connected(signaler_obj, connect_to_obj, signal_name, method_name=""): if(method_name != ""): return signaler_obj.is_connected(signal_name,Callable(connect_to_obj,method_name)) else: var connections = signaler_obj.get_signal_connection_list(signal_name) for conn in connections: if(conn['signal'].get_name() == signal_name and conn['callable'].get_object() == connect_to_obj): return true return false ## Asserts that `signaler_obj` is connected to `connect_to_obj` on signal `signal_name`. The method that is connected is optional. If `method_name` is supplied then this will pass only if the signal is connected to the method. If it is not provided then any connection to the signal will cause a pass. ## [br][br] ## [b]Signatures:[/b][br] ## - assert_connected([param p1]:Signal, [param p2]:connected-object)[br] ## - assert_connected([param p1]:Signal, [param p2]:connected-method)[br] ## - assert_connected([param p1]:object, [param p2]:connected-object, [param p3]:signal-name, [param p4]: connected-method-name ) ## [br][br] ## [b]Examples:[/b] ## [codeblock] ## class Signaler: ## signal the_signal ## ## class Connector: ## func connect_this(): ## pass ## func other_method(): ## pass ## ## func test_assert_connected(): ## var signaler = Signaler.new() ## var connector = Connector.new() ## signaler.the_signal.connect(connector.connect_this) ## ## # Passing ## assert_connected(signaler.the_signal, connector.connect_this) ## assert_connected(signaler.the_signal, connector) ## assert_connected(signaler, connector, 'the_signal') ## assert_connected(signaler, connector, 'the_signal', 'connect_this') ## ## # Failing ## assert_connected(signaler.the_signal, connector.other_method) ## ## var foo = Connector.new() ## assert_connected(signaler, connector, 'the_signal', 'other_method') ## assert_connected(signaler, connector, 'other_signal') ## assert_connected(signaler, foo, 'the_signal') ## [/codeblock] func assert_connected(p1, p2, p3=null, p4=""): var sp := SignalAssertParameters.new(p1, p3) var connect_to_obj = p2 var method_name = p4 if(connect_to_obj is Callable): method_name = connect_to_obj.get_method() connect_to_obj = connect_to_obj.get_object() var method_disp = '' if (method_name != ""): method_disp = str(' using method: [', method_name, '] ') var disp = str('Expected object ', _str(sp.object),\ ' to be connected to signal: [', sp.signal_name, '] on ',\ _str(connect_to_obj), method_disp) if(_is_connected(sp.object, connect_to_obj, sp.signal_name, method_name)): _pass(disp) else: _fail(disp) ## The inverse of [method assert_connected]. See [method assert_connected] for parameter syntax. ## [br] ## This will fail with specific messages if the target object is connected to the specified signal on the source object. func assert_not_connected(p1, p2, p3=null, p4=""): var sp := SignalAssertParameters.new(p1, p3) var connect_to_obj = p2 var method_name = p4 if(connect_to_obj is Callable): method_name = connect_to_obj.get_method() connect_to_obj = connect_to_obj.get_object() var method_disp = '' if (method_name != ""): method_disp = str(' using method: [', method_name, '] ') var disp = str('Expected object ', _str(sp.object),\ ' to not be connected to signal: [', sp.signal_name, '] on ',\ _str(sp.object), method_disp) if(_is_connected(sp.object, connect_to_obj, sp.signal_name, method_name)): _fail(disp) else: _pass(disp) ## Assert that the specified object emitted the named signal. You must call ## [method watch_signals] and pass it the object that you are making assertions about. ## This will fail if the object is not being watched or if the object does not ## have the specified signal. Since this will fail if the signal does not ## exist, you can often skip using [method assert_has_signal]. ## [br][br] ## [b]Signatures:[/b][br] ## - assert_signal_emitted([param p1]:Signal, [param p2]: text )[br] ## - assert_signal_emitted([param p1]:object, [param p2]:signal-name, [param p3]: text ) ## [br][br] ## [b]Examples:[/b] ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## ## func test_assert_signal_emitted(): ## var obj = SignalObject.new() ## ## watch_signals(obj) ## obj.emit_signal('some_signal') ## ## ## Passing ## assert_signal_emitted(obj, 'some_signal') ## assert_signal_emitted(obj.some_signal) ## ## ## Failing ## # Fails with specific message that the object does not have the signal ## assert_signal_emitted(obj, 'signal_does_not_exist') ## # Fails because the object passed is not being watched ## assert_signal_emitted(SignalObject.new(), 'some_signal') ## # Fails because the signal was not emitted ## assert_signal_emitted(obj, 'other_signal') ## assert_signal_emitted(obj.other_signal) ## [/codeblock] func assert_signal_emitted(p1, p2='', p3=""): var sp := SignalAssertParameters.new(p1, p2, p3) var disp = str('Expected object ', _str(sp.object), ' to have emitted signal [', sp.signal_name, ']: ', sp.others[0]) if(_can_make_signal_assertions(sp.object, sp.signal_name)): if(_signal_watcher.did_emit(sp.object, sp.signal_name)): _pass(disp) else: _fail(_get_fail_msg_including_emitted_signals(disp, sp.object)) ## This works opposite of `assert_signal_emitted`. This will fail if the object ## is not being watched or if the object does not have the signal. ## [br][br] ## [b]Signatures:[/b][br] ## - assert_signal_not_emitted([param p1]:Signal, [param p2]: text )[br] ## - assert_signal_not_emitted([param p1]:object, [param p2]:signal-name, [param p3]: text ) ## [br][br] ## [b]Examples:[/b] ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## func test_assert_signal_not_emitted(): ## var obj = SignalObject.new() ## ## watch_signals(obj) ## obj.emit_signal('some_signal') ## ## # Passing ## assert_signal_not_emitted(obj, 'other_signal') ## assert_signal_not_emitted(obj.other_signal) ## ## # Failing ## # Fails with specific message that the object does not have the signal ## assert_signal_not_emitted(obj, 'signal_does_not_exist') ## # Fails because the object passed is not being watched ## assert_signal_not_emitted(SignalObject.new(), 'some_signal') ## # Fails because the signal was emitted ## assert_signal_not_emitted(obj, 'some_signal') ## [/codeblock] func assert_signal_not_emitted(p1, p2='', p3=''): var sp := SignalAssertParameters.new(p1, p2, p3) var disp = str('Expected object ', _str(sp.object), ' to NOT emit signal [', sp.signal_name, ']: ', sp.others[0]) if(_can_make_signal_assertions(sp.object, sp.signal_name)): if(_signal_watcher.did_emit(sp.object, sp.signal_name)): _fail(disp) else: _pass(disp) ## Asserts that a signal was fired with the specified parameters. The expected ## parameters should be passed in as an array. An optional index can be passed ## when a signal has fired more than once. The default is to retrieve the most ## recent emission of the signal. ## [br] ## This will fail with specific messages if the object is not being watched or ## the object does not have the specified signal ## [br][br] ## [b]Signatures:[/b][br] ## - assert_signal_emitted_with_parameters([param p1]:Signal, [param p2]:expected-parameters, [param p3]: index )[br] ## - assert_signal_emitted_with_parameters([param p1]:object, [param p2]:signal-name, [param p3]:expected-parameters, [param p4]: index ) ## [br][br] ## [b]Examples:[/b] ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## func test_assert_signal_emitted_with_parameters(): ## var obj = SignalObject.new() ## ## watch_signals(obj) ## # emit the signal 3 times to illustrate how the index works in ## # assert_signal_emitted_with_parameters ## obj.emit_signal('some_signal', 1, 2, 3) ## obj.emit_signal('some_signal', 'a', 'b', 'c') ## obj.emit_signal('some_signal', 'one', 'two', 'three') ## ## # Passing ## # Passes b/c the default parameters to check are the last emission of ## # the signal ## assert_signal_emitted_with_parameters(obj, 'some_signal', ['one', 'two', 'three']) ## assert_signal_emitted_with_parameters(obj.some_signal, ['one', 'two', 'three']) ## ## # Passes because the parameters match the specified emission based on index. ## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 0) ## assert_signal_emitted_with_parameters(obj.some_signal, [1, 2, 3], 0) ## ## # Failing ## # Fails with specific message that the object does not have the signal ## assert_signal_emitted_with_parameters(obj, 'signal_does_not_exist', []) ## # Fails because the object passed is not being watched ## assert_signal_emitted_with_parameters(SignalObject.new(), 'some_signal', []) ## # Fails because parameters do not match latest emission ## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3]) ## # Fails because the parameters for the specified index do not match ## assert_signal_emitted_with_parameters(obj, 'some_signal', [1, 2, 3], 1) ## [/codeblock] func assert_signal_emitted_with_parameters(p1, p2, p3=-1, p4=-1): var sp := SignalAssertParameters.new(p1, p2, p3, p4) var parameters = sp.others[0] var index = sp.others[1] if(typeof(parameters) != TYPE_ARRAY): _lgr.error("The expected parameters must be wrapped in an array, you passed: " + _str(parameters)) _fail("Bad Parameters") return var disp = str('Expected object ', _str(sp.object), ' to emit signal [', sp.signal_name, '] with parameters ', parameters, ', got ') if(_can_make_signal_assertions(sp.object, sp.signal_name)): if(_signal_watcher.did_emit(sp.object, sp.signal_name)): var parms_got = _signal_watcher.get_signal_parameters(sp.object, sp.signal_name, index) var diff_result = _compare.deep(parameters, parms_got) if(diff_result.are_equal): _pass(str(disp, parms_got)) else: _fail(str('Expected object ', _str(sp.object), ' to emit signal [', sp.signal_name, '] with parameters ', diff_result.summarize())) else: var text = str('Object ', sp.object, ' did not emit signal [', sp.signal_name, ']') _fail(_get_fail_msg_including_emitted_signals(text, sp.object)) ## Asserts that a signal fired a specific number of times. ## [br][br] ## [b]Signatures:[/b][br] ## - assert_signal_emit_count([param p1]:Signal, [param p2]:expected-count, [param p3]: text )[br] ## - assert_signal_emit_count([param p1]:object, [param p2]:signal-name, [param p3]:expected-count, [param p4]: text ) ## [br][br] ## [b]Examples:[/b] ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## ## func test_assert_signal_emit_count(): ## var obj_a = SignalObject.new() ## var obj_b = SignalObject.new() ## ## watch_signals(obj_a) ## watch_signals(obj_b) ## ## obj_a.emit_signal('some_signal') ## obj_a.emit_signal('some_signal') ## ## obj_b.emit_signal('some_signal') ## obj_b.emit_signal('other_signal') ## ## # Passing ## assert_signal_emit_count(obj_a, 'some_signal', 2, 'passes') ## assert_signal_emit_count(obj_a.some_signal, 2, 'passes') ## ## assert_signal_emit_count(obj_a, 'other_signal', 0) ## assert_signal_emit_count(obj_a.other_signal, 0) ## ## assert_signal_emit_count(obj_b, 'other_signal', 1) ## ## # Failing ## # Fails with specific message that the object does not have the signal ## assert_signal_emit_count(obj_a, 'signal_does_not_exist', 99) ## # Fails because the object passed is not being watched ## assert_signal_emit_count(SignalObject.new(), 'some_signal', 99) ## # The following fail for obvious reasons ## assert_signal_emit_count(obj_a, 'some_signal', 0) ## assert_signal_emit_count(obj_b, 'other_signal', 283) ## [/codeblock] func assert_signal_emit_count(p1, p2, p3=0, p4=""): var sp := SignalAssertParameters.new(p1, p2, p3, p4) var times = sp.others[0] var text = sp.others[1] if(_can_make_signal_assertions(sp.object, sp.signal_name)): var count = _signal_watcher.get_emit_count(sp.object, sp.signal_name) var disp = str('Expected the signal [', sp.signal_name, '] emit count of [', count, '] to equal [', times, ']: ', text) if(count== times): _pass(disp) else: _fail(_get_fail_msg_including_emitted_signals(disp, sp.object)) ## Asserts the passed in object has a signal with the specified name. It ## should be noted that all the asserts that verify a signal was/wasn't emitted ## will first check that the object has the signal being asserted against. If ## it does not, a specific failure message will be given. This means you can ## usually skip the step of specifically verifying that the object has a signal ## and move on to making sure it emits the signal correctly. ## [codeblock] ## class SignalObject: ## signal some_signal ## signal other_signal ## ## func test_assert_has_signal(): ## var obj = SignalObject.new() ## ## ## Passing ## assert_has_signal(obj, 'some_signal') ## assert_has_signal(obj, 'other_signal') ## ## ## Failing ## assert_has_signal(obj, 'not_a real SIGNAL') ## assert_has_signal(obj, 'yea, this one doesnt exist either') ## # Fails because the signal is not a user signal. Node2D does have the ## # specified signal but it can't be checked this way. It could be watched ## # and asserted that it fired though. ## assert_has_signal(Node2D.new(), 'exit_tree') ## [/codeblock] func assert_has_signal(object, signal_name, text=""): var disp = str('Expected object ', _str(object), ' to have signal [', signal_name, ']: ', text) if(_signal_watcher.does_object_have_signal(object, signal_name)): _pass(disp) else: _fail(disp) ## Asserts that [param object] extends [param a_class]. object must be an instance of an ## object. It cannot be any of the built in classes like Array or Int or Float. ## [param a_class] must be a class, it can be loaded via load, a GDNative class such as ## Node or Label or anything else. ## [codeblock] ## # Passing ## assert_is(Node2D.new(), Node2D) ## assert_is(Label.new(), CanvasItem) ## assert_is(SubClass.new(), BaseClass) ## # Since this is a test script that inherits from test.gd, so ## # this passes. It's not obvious w/o seeing the whole script ## # so I'm telling you. You'll just have to trust me. ## assert_is(self, load('res://addons/gut/test.gd')) ## ## var Gut = load('res://addons/gut/gut.gd') ## var a_gut = Gut.new() ## assert_is(a_gut, Gut) ## ## # Failing ## assert_is(Node2D.new(), Node2D.new()) ## assert_is(BaseClass.new(), SubClass) ## assert_is('a', 'b') ## assert_is([], Node) ## [/codeblock] func assert_is(object, a_class, text=''): var disp = ''#var disp = str('Expected [', _str(object), '] to be type of [', a_class, ']: ', text) var bad_param_2 = 'Parameter 2 must be a Class (like Node2D or Label). You passed ' if(typeof(object) != TYPE_OBJECT): _fail(str('Parameter 1 must be an instance of an object. You passed: ', _str(object))) elif(typeof(a_class) != TYPE_OBJECT): _fail(str(bad_param_2, _str(a_class))) else: var a_str = _str(a_class) disp = str('Expected [', _str(object), '] to extend [', a_str, ']: ', text) if(!GutUtils.is_native_class(a_class) and !GutUtils.is_gdscript(a_class)): _fail(str(bad_param_2, a_str)) else: if(is_instance_of(object, a_class)): _pass(disp) else: _fail(disp) ## Asserts that [param object] is the the [param type] specified. [param type] ## should be one of the Godot [code]TYPE_[/code] constants. ## [codeblock] ## # Passing ## var c = Color(1, 1, 1, 1) ## gr.test.assert_typeof(c, TYPE_COLOR) ## assert_pass(gr.test) ## ## # Failing ## gr.test.assert_typeof('some string', TYPE_INT) ## assert_fail(gr.test) ## [/codeblock] func assert_typeof(object, type, text=''): var disp = str('Expected [typeof(', object, ') = ') disp += _get_typeof_string(typeof(object)) disp += '] to equal [' disp += _get_typeof_string(type) + ']' disp += '. ' + text if(typeof(object) == type): _pass(disp) else: _fail(disp) ## The inverse of [method assert_typeof] func assert_not_typeof(object, type, text=''): var disp = str('Expected [typeof(', object, ') = ') disp += _get_typeof_string(typeof(object)) disp += '] to not equal [' disp += _get_typeof_string(type) + ']' disp += '. ' + text if(typeof(object) != type): _pass(disp) else: _fail(disp) ## Assert that `text` contains `search`. Can perform case insensitive search ## by passing false for `match_case`. ## [codeblock] ## # Passing ## assert_string_contains('abc 123', 'a') ## assert_string_contains('abc 123', 'BC', false) ## assert_string_contains('abc 123', '3') ## ## # Failing ## assert_string_contains('abc 123', 'A') ## assert_string_contains('abc 123', 'BC') ## assert_string_contains('abc 123', '012') ## [/codeblock] func assert_string_contains(text, search, match_case=true): const empty_search = 'Expected text and search strings to be non-empty. You passed %s and %s.' const non_strings = 'Expected text and search to both be strings. You passed %s and %s.' var disp = 'Expected \'%s\' to contain \'%s\', match_case=%s' % [text, search, match_case] if(typeof(text) != TYPE_STRING or typeof(search) != TYPE_STRING): _fail(non_strings % [_str(text), _str(search)]) elif(text == '' or search == ''): _fail(empty_search % [_str(text), _str(search)]) elif(match_case): if(text.find(search) == -1): _fail(disp) else: _pass(disp) else: if(text.to_lower().find(search.to_lower()) == -1): _fail(disp) else: _pass(disp) ## Assert that text starts with search. Can perform case insensitive check ## by passing false for match_case ## [codeblock] ## # Passing ## assert_string_starts_with('abc 123', 'a') ## assert_string_starts_with('abc 123', 'ABC', false) ## assert_string_starts_with('abc 123', 'abc 123') ## ## ## Failing ## assert_string_starts_with('abc 123', 'z') ## assert_string_starts_with('abc 123', 'ABC') ## assert_string_starts_with('abc 123', 'abc 1234') ## [/codeblock] func assert_string_starts_with(text, search, match_case=true): var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' var disp = 'Expected \'%s\' to start with \'%s\', match_case=%s' % [text, search, match_case] if(text == '' or search == ''): _fail(empty_search % [text, search]) elif(match_case): if(text.find(search) == 0): _pass(disp) else: _fail(disp) else: if(text.to_lower().find(search.to_lower()) == 0): _pass(disp) else: _fail(disp) ## Assert that [param text] ends with [param search]. Can perform case insensitive check by passing false for [param match_case] ## [codeblock] ## ## Passing ## assert_string_ends_with('abc 123', '123') ## assert_string_ends_with('abc 123', 'C 123', false) ## assert_string_ends_with('abc 123', 'abc 123') ## ## ## Failing ## assert_string_ends_with('abc 123', '1234') ## assert_string_ends_with('abc 123', 'C 123') ## assert_string_ends_with('abc 123', 'nope') ## [/codeblock] func assert_string_ends_with(text, search, match_case=true): var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' var disp = 'Expected \'%s\' to end with \'%s\', match_case=%s' % [text, search, match_case] var required_index = len(text) - len(search) if(text == '' or search == ''): _fail(empty_search % [text, search]) elif(match_case): if(text.find(search) == required_index): _pass(disp) else: _fail(disp) else: if(text.to_lower().find(search.to_lower()) == required_index): _pass(disp) else: _fail(disp) # ------------------------------------------------------------------------------ ## Assert that a method was called on an instance of a doubled class. If ## parameters are supplied then the params passed in when called must match. ## ## Can be called with a Callabe instead of specifying the object, method_name, ## and parameters. The Callable's object must be a double. Bound arguments ## will be used to match calls based on values passed to the method. ## [br] ## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] ## [br][br] ## [b]Examples[/b] ## [codeblock] ## var my_double = double(Foobar).new() ## ... ## assert_called(my_double, 'foo') ## assert_called(my_double.foo) ## assert_called(my_double, 'foo', [1, 2, 3]) ## assert_called(my_double.foo.bind(1, 2, 3)) ## [/codeblock] func assert_called(inst, method_name=null, parameters=null): if(_fail_if_parameters_not_array(parameters)): return var converted = _convert_spy_args(inst, method_name, parameters) if(converted.invalid_message != 'ok'): fail_test(converted.invalid_message) return var disp = str('Expected [',converted.method_name,'] to have been called on ',_str(converted.object)) if(converted.arguments != null): disp += str(' with parameters ', converted.arguments) if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): if(gut.get_spy().was_called( converted.object, converted.method_name, converted.arguments)): _pass(disp) else: _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) # ------------------------------------------------------------------------------ ## Assert that a method was not called on an instance of a doubled class. If ## parameters are specified then this will only fail if it finds a call that was ## sent matching parameters. ## ## Can be called with a Callabe instead of specifying the object, method_name, ## and parameters. The Callable's object must be a double. Bound arguments ## will be used to match calls based on values passed to the method. ## [br] ## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] ## [br][br] ## [b]Examples[/b] ## [codeblock] ## assert_not_called(my_double, 'foo') ## assert_not_called(my_double.foo) ## assert_not_called(my_double, 'foo', [1, 2, 3]) ## assert_not_called(my_double.foo.bind(1, 2, 3)) ## [/codeblock] func assert_not_called(inst, method_name=null, parameters=null): if(_fail_if_parameters_not_array(parameters)): return var converted = _convert_spy_args(inst, method_name, parameters) if(converted.invalid_message != 'ok'): fail_test(converted.invalid_message) return var disp = str('Expected [', converted.method_name, '] to NOT have been called on ', _str(converted.object)) if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): if(gut.get_spy().was_called( converted.object, converted.method_name, converted.arguments)): if(converted.arguments != null): disp += str(' with parameters ', converted.arguments) _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) else: _pass(disp) ## Asserts the the method of a double was called an expected number of times. ## If any arguments are bound to the callable then only calls with matching ## arguments will be counted. ## [br] ## See also: [wiki]Doubles[/wiki], [wiki]Spies[/wiki] ## [br][br] ## [b]Examples[/b] ## [codeblock] ## # assert foo was called on my_double 5 times ## assert_called_count(my_double.foo, 5) ## # assert foo, with parameters [1,2,3], was called on my_double 4 times. ## assert_called_count(my_double.foo.bind(1, 2, 3), 4) ## [/codeblock] func assert_called_count(callable : Callable, expected_count : int): var converted = _convert_spy_args(callable, null, null) var count = gut.get_spy().call_count(converted.object, converted.method_name, converted.arguments) var param_text = '' if(callable.get_bound_arguments_count() > 0): param_text = ' with parameters ' + str(callable.get_bound_arguments()) var disp = 'Expected [%s] on %s to be called [%s] times%s. It was called [%s] times.' disp = disp % [converted.method_name, _str(converted.object), expected_count, param_text, count] if(_fail_if_not_double_or_does_not_have_method(converted.object, converted.method_name) == OK): if(count == expected_count): _pass(disp) else: _fail(str(disp, "\n", _get_desc_of_calls_to_instance(converted.object))) ## Asserts the passed in value is null func assert_null(got, text=''): var disp = str('Expected [', _str(got), '] to be NULL: ', text) if(got == null): _pass(disp) else: _fail(disp) ## Asserts the passed in value is not null. func assert_not_null(got, text=''): var disp = str('Expected [', _str(got), '] to be anything but NULL: ', text) if(got == null): _fail(disp) else: _pass(disp) ## Asserts that the passed in object has been freed. This assertion requires ## that you pass in some text in the form of a title since, if the object is ## freed, we won't have anything to convert to a string to put in the output ## statement. ## [br] ## [b]Note[/b] that this currently does not detect if a node has been queued free. ## [codeblock] ## var obj = Node.new() ## obj.free() ## test.assert_freed(obj, "New Node") ## [/codeblock] func assert_freed(obj, title='something'): var disp = title if(is_instance_valid(obj)): disp = _strutils.type2str(obj) + title assert_true(not is_instance_valid(obj), "Expected [%s] to be freed" % disp) ## The inverse of [method assert_freed] func assert_not_freed(obj, title='something'): var disp = title if(is_instance_valid(obj)): disp = _strutils.type2str(obj) + title assert_true(is_instance_valid(obj), "Expected [%s] to not be freed" % disp) ## This method will assert that no orphaned nodes have been introduced by the ## test when the assert is executed. See the [wiki]Memory-Management[/wiki] ## page for more information. func assert_no_new_orphans(text=''): var orphan_ids = gut.get_current_test_orphans() var count = orphan_ids.size() var msg = '' if(text != ''): msg = ': ' + text # Note that get_counter will return -1 if the counter does not exist. This # can happen with a misplaced assert_no_new_orphans. Checking for > 0 # ensures this will not cause some weird failure. if(count > 0): msg += str("\n", _strutils.indent_text(gut.get_orphan_counter().get_orphan_list_text(orphan_ids), 1, ' ')) _fail(str('Expected no orphans, but found ', count, msg)) else: _pass('No new orphans found.' + msg) ## @ignore func assert_set_property(obj, property_name, new_value, expected_value): pending("this hasn't been implemented yet") ## @ignore func assert_readonly_property(obj, property_name, new_value, expected_value): pending("this hasn't been implemented yet") ## Assumes backing varible with be _. This will perform all the ## asserts of assert_property. Then this will set the value through the setter ## and check the backing variable value. It will then reset throught the setter ## and set the backing variable and check the getter. func assert_property_with_backing_variable(obj, property_name, default_value, new_value, backed_by_name=null): var setter_name = str('@', property_name, '_setter') var getter_name = str('@', property_name, '_getter') var backing_name = GutUtils.nvl(backed_by_name, str('_', property_name)) var pre_fail_count = get_fail_count() var props = obj.get_property_list() var found = false var idx = 0 while(idx < props.size() and !found): found = props[idx].name == backing_name idx += 1 assert_true(found, str(obj, ' has ', backing_name, ' variable.')) assert_true(obj.has_method(setter_name), str('There should be a setter for ', property_name)) assert_true(obj.has_method(getter_name), str('There should be a getter for ', property_name)) if(pre_fail_count == get_fail_count()): var call_setter = Callable(obj, setter_name) var call_getter = Callable(obj, getter_name) assert_eq(obj.get(backing_name), default_value, str('Variable ', backing_name, ' has default value.')) assert_eq(call_getter.call(), default_value, 'Getter returns default value.') call_setter.call(new_value) assert_eq(call_getter.call(), new_value, 'Getter returns value from Setter.') assert_eq(obj.get(backing_name), new_value, str('Variable ', backing_name, ' was set')) _warn_for_public_accessors(obj, property_name) ## This will verify that the method has a setter and getter for the property. ## It will then use the getter to check the default. Then use the ## setter with new_value and verify the getter returns the same value. func assert_property(obj, property_name, default_value, new_value) -> void: var pre_fail_count = get_fail_count() var setter_name = str('@', property_name, '_setter') var getter_name = str('@', property_name, '_getter') if(typeof(obj) != TYPE_OBJECT): _fail(str(_str(obj), ' is not an object')) return assert_has_method(obj, setter_name) assert_has_method(obj, getter_name) if(pre_fail_count == get_fail_count()): var call_setter = Callable(obj, setter_name) var call_getter = Callable(obj, getter_name) assert_eq(call_getter.call(), default_value, 'Default value') call_setter.call(new_value) assert_eq(call_getter.call(), new_value, 'Getter gets Setter value') _warn_for_public_accessors(obj, property_name) ## Performs a deep comparison between two arrays or dictionaries and asserts ## they are equal. If they are not equal then a formatted list of differences ## are displayed. See [wiki]Comparing-Things[/wiki] for more information. func assert_eq_deep(v1, v2): var result = compare_deep(v1, v2) if(result.are_equal): _pass(result.get_short_summary()) else: _fail(result.summary) ## Performs a deep comparison of two arrays or dictionaries and asserts they ## are not equal. See [wiki]Comparing-Things[/wiki] for more information. func assert_ne_deep(v1, v2): var result = compare_deep(v1, v2) if(!result.are_equal): _pass(result.get_short_summary()) else: _fail(result.get_short_summary()) ## Assert v1 and v2 are the same using [code]is_same[/code]. See @GlobalScope.is_same. func assert_same(v1, v2, text=''): var disp = "[" + _str(v1) + "] expected to be same as [" + _str(v2) + "]: " + text if(is_same(v1, v2)): _pass(disp) else: _fail(disp) ## Assert using v1 and v2 are not the same using [code]is_same[/code]. See @GlobalScope.is_same. func assert_not_same(v1, v2, text=''): var disp = "[" + _str(v1) + "] expected to not be same as [" + _str(v2) + "]: " + text if(is_same(v1, v2)): _fail(disp) else: _pass(disp) # ---------------- #endregion #region Error Detection # ---------------- var _error_type_check_methods = { "push_error": "is_push_error", "engine": "is_engine_error", } # smells like GutTrackedError needs some more constants but I'm not ready to # make them yet func _is_error_of_type(err, error_type_name): return err.call(_error_type_check_methods[error_type_name]) func _assert_error_count(count, error_type_name, msg): var consumed_count = 0 var errors = gut.error_tracker.get_errors_for_test() var found = [] var disp = msg for err in errors: if(_is_error_of_type(err, error_type_name)): if(consumed_count < count): err.handled = true consumed_count += 1 found.append(err) if(disp != ''): disp = str(': ', disp) else: disp = '.' disp = str("Expected ", count, " ", error_type_name, " errors. Got ", found.size(), disp) if(found.size() == count): _pass(disp) if(!_lgr.is_type_enabled(_lgr.types.passed)): _lgr.expected_error(msg) else: _fail(disp) func _assert_error_text(text, error_type_name, msg): var consumed_count = 0 var errors = gut.error_tracker.get_errors_for_test() var found = [] var disp = msg for err in errors: if(_is_error_of_type(err, error_type_name) and err.contains_text(text)): if(consumed_count == 0): err.handled = true consumed_count += 1 found.append(err) disp = str("Expected ", error_type_name, " error containing '", text, "'. ", msg) if(consumed_count == 1): _pass(disp) if(!_lgr.is_type_enabled(_lgr.types.passed)): _lgr.expected_error(disp) else: _fail(disp) ## Get all the errors in the test up to this point. Each error is an instance ## of [GutTrackedError]. Setting the [member GutTrackedError.handled] [code]handled[/code] property of ## an element in the array will prevent it from causing a test to fail. ## [br][br] ## This method allows you to inspect the details of any errors that occured and ## decide if it's the error you are expecting or not. ## [br][br] ## [codeblock] ## func divide_them(a, b): ## return a / b ## ## func test_with_script_error(): ## divide_them('one', 44) ## push_error('this is a push error') ## var errs = get_errors() ## assert_eq(errs.size(), 2, 'expected error count') ## ## # Maybe inspect some properties of the errors here. ## ## # Mark all the errors as handled. ## for e in errs: ## e.handled = true ## [/codeblock] ## See [GutTrackedError], [wiki]Error-Tracking[/wiki]. func get_errors()->Array: return gut.error_tracker.get_errors_for_test() ## Asserts that a number of engine or a single engine error continating ## (case insensitive) text has occurred. If the expected error(s) are ## found then this assert will pass and the test will not fail from an ## unexpected push_error. ## [br][br] ## This assert will pass/fail even if push_errors are not configured to cause ## a test failure. This will not prevent the error from showing up in output. ## [br][br] ## [codeblock] ## func divide_them(a, b): ## return a / b ## ## func test_asserting_engine_error_count(): ## divide_them('one', 44) ## assert_engine_error(1, "expecing a script error") ## ## func test_asserting_engine_error_text(): ## divide_them('word', 91) ## assert_engine_error('invalid operands') ## ## func test_asserting_multipe_engine_error_texts(): ## divide_them('foo', Node) ## divide_them(1729, 0) ## assert_engine_error('Division by zero') ## assert_engine_error('invalid operands') ## [/codeblock] ## See [wiki]Error-Tracking[/wiki]. func assert_engine_error(count_or_text, msg=''): var t = typeof(count_or_text) if(t == TYPE_INT or t == TYPE_FLOAT): _assert_error_count(count_or_text, "engine", msg) elif(t == TYPE_STRING): _assert_error_text(count_or_text, 'engine', msg) else: _fail(str("Unexpected input: ", count_or_text)) ## Asserts that a number of push_errors or a single push error continating ## (case insensitive) text has occurred. If the expected error(s) are ## found then this assert will pass and the test will not fail from an ## unexpected push_error. ## [br][br] ## This assert will pass/fail even if push_errors are not configured to cause ## a test failure. This will not prevent the error from showing up in output. ## [codeblock] ## func test_with_push_error(): ## push_error("This is an error") ## assert_push_error(1, 'This test should have caused a push_error) ## ## func test_push_error_text(): ## push_error("SpecialText") ## assert_push_error("CIALtex") ## ## func test_push_error_multiple_texts(): ## push_error("Error One") ## push_error("Expception two") ## assert_push_error("one") ## assert_push_error("two") ## ## [/codeblock] ## See [wiki]Error-Tracking[/wiki]. func assert_push_error(count_or_text, msg=''): var t = typeof(count_or_text) if(t == TYPE_INT or t == TYPE_FLOAT): _assert_error_count(count_or_text, "push_error", msg) elif(t == TYPE_STRING): _assert_error_text(count_or_text, 'push_error', msg) else: _fail(str("Unexpected input: ", count_or_text)) # ---------------- #endregion #region Await Helpers # ---------------- ## Use with await to wait an amount of time in seconds. The optional message ## will be printed when the await starts.[br] ## See [wiki]Awaiting[/wiki] func wait_seconds(time, msg=''): _awaiter.wait_seconds(time) return _awaiter.timeout ## Use with await to wait for a signal to be emitted or a maximum amount of ## time. Returns true if the signal was emitted, false if not.[br] ## See [wiki]Awaiting[/wiki] func wait_for_signal(sig : Signal, max_time, msg=''): watch_signals(sig.get_object()) _awaiter.wait_for_signal(sig, max_time, msg) await _awaiter.timeout return !_awaiter.did_last_wait_timeout ## @deprecated ## Use wait_physics_frames or wait_process_frames ## See [wiki]Awaiting[/wiki]. func wait_frames(frames : int, msg=''): _lgr.deprecated("wait_frames has been replaced with wait_physics_frames which is counted in _physics_process. " + "wait_process_frames has also been added which is counted in _process.") return wait_physics_frames(frames, msg) ## This returns a signal that is emitted after [param x] physics frames have ## elpased. You can await this method directly to pause execution for [param x] ## physics frames. The frames are counted prior to _physics_process being called ## on any node (when [signal SceneTree.physics_frame] is emitted). This means the ## signal is emitted after [param x] frames and just before the x + 1 frame starts. ## [codeblock] ## await wait_physics_frames(10) ## [/codeblock] ## See [wiki]Awaiting[/wiki] func wait_physics_frames(x :int , msg=''): if(x <= 0): var text = str('wait_physics_frames: frames must be > 0, you passed ', x, '. 1 frames waited.') _lgr.error(text) x = 1 _awaiter.wait_physics_frames(x, msg) return _awaiter.timeout ## Alias for [method GutTest.wait_process_frames] func wait_idle_frames(x : int, msg=''): return wait_process_frames(x, msg) ## This returns a signal that is emitted after [param x] process/idle frames have ## elpased. You can await this method directly to pause execution for [param x] ## process/idle frames. The frames are counted prior to _process being called ## on any node (when [signal SceneTree.process_frame] is emitted). This means the ## signal is emitted after [param x] frames and just before the x + 1 frame starts. ## [codeblock] ## await wait_process_frames(10) ## # wait_idle_frames is an alias of wait_process_frames ## await wait_idle_frames(10) ## [/codeblock] ## See [wiki]Awaiting[/wiki] func wait_process_frames(x : int, msg=''): if(x <= 0): var text = str('wait_process_frames: frames must be > 0, you passed ', x, '. 1 frames waited.') _lgr.error(text) x = 1 _awaiter.wait_process_frames(x, msg) return _awaiter.timeout ## Use with await to wait for [param callable] to return the boolean value ## [code]true[/code] or a maximum amount of time. All values that are not the ## boolean value [code]true[/code] are ignored. [param callable] is called ## every [code]_physics_process[/code] tick unless an optional time between ## calls is specified.[br] ## [param p3] can be the optional message or an amount of time to wait between calls.[br] ## [param p4] is the optional message if you have specified an amount of time to ## wait between calls.[br] ## Returns [code]true[/code] if [param callable] returned true before the timeout, false if not. ##[br] ##[codeblock] ## var foo = 1 ## func test_example(): ## var foo_func = func(): ## foo += 1 ## return foo == 10 ## foo = 1 ## wait_until(foo_func, 5, 'optional message') ## # or give it a time between ## foo = 1 ## wait_until(foo_func, 5, 1, ## 'this will timeout because we call it every second and are waiting a max of 10 seconds') ## ##[/codeblock] ## See also [method wait_while][br] ## See [wiki]Awaiting[/wiki] func wait_until(callable, max_time, p3='', p4=''): var time_between = 0.0 var message = p4 if(typeof(p3) != TYPE_STRING): time_between = p3 else: message = p3 _awaiter.wait_until(callable, max_time, time_between, message) await _awaiter.timeout return !_awaiter.did_last_wait_timeout ## This is the inverse of [method wait_until]. This will continue to wait while ## [param callable] returns the boolean value [code]true[/code]. If [b]ANY[/b] ## other value is is returned then the wait will end. ## Returns [code]true[/code] if [param callable] returned a value other than ## [code]true[/code] before the timeout, [code]false[/code] if not. ##[codeblock] ## var foo = 1 ## func test_example(): ## var foo_func = func(): ## foo += 1 ## if(foo < 10): ## return true ## else: ## return 'this is not a boolean' ## foo = 1 ## wait_while(foo_func, 5, 'optional message') ## # or give it a time between ## foo = 1 ## wait_while(foo_func, 5, 1, ## 'this will timeout because we call it every second and are waiting a max of 10 seconds') ## ##[/codeblock] ## See [wiki]Awaiting[/wiki] func wait_while(callable, max_time, p3='', p4=''): var time_between = 0.0 var message = p4 if(typeof(p3) != TYPE_STRING): time_between = p3 else: message = p3 _awaiter.wait_while(callable, max_time, time_between, message) await _awaiter.timeout return !_awaiter.did_last_wait_timeout ## Returns whether the last wait_* method timed out. This is always true if ## the last method was wait_xxx_frames or wait_seconds. It will be false when ## using wait_for_signal and wait_until if the timeout occurs before what ## is being waited on. The wait_* methods return this value so you should be ## able to avoid calling this directly, but you can. func did_wait_timeout(): return _awaiter.did_last_wait_timeout # ---------------- #endregion #region Summary Data # ---------------- ## @internal func get_summary(): return _summary ## Returns the number of failing asserts in this script at the time this ## method was called. Call in [method after_all] to get total count for script. func get_fail_count(): return _summary.failed ## Returns the number of passing asserts in this script at the time this method ## was called. Call in [method after_all] to get total count for script. func get_pass_count(): return _summary.passed ## Returns the number of pending tests in this script at the time this method ## was called. Call in [method after_all] to get total count for script. func get_pending_count(): return _summary.pending ## Returns the total number of asserts this script has made as of the time of ## this was called. Call in [method after_all] to get total count for script. func get_assert_count(): return _summary.asserts # Convert the _summary dictionary into text ## @internal func get_summary_text(): var to_return = get_script().get_path() + "\n" to_return += str(' ', _summary.passed, ' of ', _summary.asserts, ' passed.') if(_summary.pending > 0): to_return += str("\n ", _summary.pending, ' pending') if(_summary.failed > 0): to_return += str("\n ", _summary.failed, ' failed.') return to_return # ---------------- #endregion #region Double Methods # ---------------- ## Create a Double of [param thing]. [param thing] should be a Class, script, ## or scene. See [wiki]Doubles[/wiki] func double(thing, double_strat=null, not_used_anymore=null): if(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)): return null return _smart_double(thing, double_strat, false) ## Create a Partial Double of [param thing]. [param thing] should be a Class, ## script, or scene. See [wiki]Partial-Doubles[/wiki] func partial_double(thing, double_strat=null, not_used_anymore=null): if(!_are_double_parameters_valid(thing, double_strat, not_used_anymore)): return null return _smart_double(thing, double_strat, true) ## @internal func double_singleton(singleton_name): return null # var to_return = null # if(_validate_singleton_name(singleton_name)): # to_return = gut.get_doubler().double_singleton(singleton_name) # return to_return ## @internal func partial_double_singleton(singleton_name): return null # var to_return = null # if(_validate_singleton_name(singleton_name)): # to_return = gut.get_doubler().partial_double_singleton(singleton_name) # return to_return ## This was implemented to allow the doubling of classes with static methods. ## There might be other valid use cases for this method, but you should always ## try stubbing before using this method. Using ## [code]stub(my_double, 'method').to_call_super()[/code] or creating a ## [method partial_double] works for any other known scenario. You cannot stub ## or spy on methods passed to [code skip-lint]ignore_method_when_doubling[/code]. func ignore_method_when_doubling(thing, method_name): if(typeof(thing) == TYPE_STRING): _lgr.error('ignore_method_when_doubling no longer supports paths to scripts or scenes. Load them and pass them instead.') return var r = thing if(thing is PackedScene): r = GutUtils.get_scene_script_object(thing) gut.get_doubler().add_ignored_method(r, method_name) ## Stub something. See [wiki]Stubbing[/wiki] for detailed information about stubbing. func stub(thing, p2=null, p3=null): var method_name = p2 var subpath = null if(p3 != null): subpath = p2 method_name = p3 if(GutUtils.is_instance(thing) and !GutUtils.is_double(thing)): _lgr.error(str("An instance of a Double was expected, you passed: ", _str(thing))) return GutUtils.StubParams.new() var sp = null if(typeof(thing) == TYPE_CALLABLE): if(p2 != null or p3 != null): _lgr.error("Only one parameter expected when using a callable.") sp = GutUtils.StubParams.new(thing) else: sp = GutUtils.StubParams.new(thing, method_name, subpath) if(GutUtils.is_instance(sp.stub_target)): var msg = _get_bad_method_message(sp.stub_target, sp.stub_method, 'stub') if(msg != ''): _lgr.error(msg) return GutUtils.StubParams.new() sp.logger = _lgr gut.get_stubber().add_stub(sp) return sp # ---------------- #endregion #region Memory Mgmt # ---------------- ## Marks whatever is passed in to be freed after the test finishes. It also ## returns what is passed in so you can save a line of code. ## var thing = autofree(Thing.new()) func autofree(thing): gut.get_autofree().add_free(thing) return thing ## Works the same as autofree except queue_free will be called on the object ## instead. This also imparts a brief pause after the test finishes so that ## the queued object has time to free. func autoqfree(thing): gut.get_autofree().add_queue_free(thing) return thing ## The same as autofree but it also adds the object as a child of the test. func add_child_autofree(node, legible_unique_name = false): gut.get_autofree().add_free(node) # Explicitly calling super here b/c add_child MIGHT change and I don't want # a bug sneaking its way in here. super.add_child(node, legible_unique_name) return node ## The same as autoqfree but it also adds the object as a child of the test. func add_child_autoqfree(node, legible_unique_name=false): gut.get_autofree().add_queue_free(node) # Explicitly calling super here b/c add_child MIGHT change and I don't want # a bug sneaking its way in here. super.add_child(node, legible_unique_name) return node # ---------------- #endregion #region Deprecated/Removed # ---------------- ## REMOVED ## @ignore func compare_shallow(v1, v2, max_differences=null): _fail('compare_shallow has been removed. Use compare_deep or just compare using == instead.') _lgr.error('compare_shallow has been removed. Use compare_deep or just compare using == instead.') return null ## REMOVED ## @ignore func assert_eq_shallow(v1, v2): _fail('assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep') ## REMOVED ## @ignore func assert_ne_shallow(v1, v2): _fail('assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep') ## @deprecated: use wait_seconds func yield_for(time, msg=''): _lgr.deprecated('yield_for', 'wait_seconds') return wait_seconds(time, msg) ## @deprecated: use wait_for_signal func yield_to(obj, signal_name, max_wait, msg=''): _lgr.deprecated('yield_to', 'wait_for_signal') return await wait_for_signal(Signal(obj, signal_name), max_wait, msg) ## @deprecated: use wait_frames func yield_frames(frames, msg=''): _lgr.deprecated("yield_frames", "wait_frames") return wait_frames(frames, msg) ## @deprecated: no longer supported. Use double func double_scene(path, strategy=null): _lgr.deprecated('test.double_scene has been removed.', 'double') return null ## @deprecated: no longer supported. Use double func double_script(path, strategy=null): _lgr.deprecated('test.double_script has been removed.', 'double') return null # var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy()) # return gut.get_doubler().double(path, override_strat) ## @deprecated: no longer supported. Use register_inner_classes + double func double_inner(path, subpath, strategy=null): _lgr.deprecated('double_inner should not be used. Use register_inner_classes and double instead.', 'double') return null var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy()) return gut.get_doubler().double_inner(path, subpath, override_strat) ## @deprecated: Use [method assert_called_count] instead. func assert_call_count(inst, method_name, expected_count, parameters=null): gut.logger.deprecated('This has been replaced with assert_called_count which accepts a Callable with optional bound arguments.') var callable = Callable.create(inst, method_name) if(parameters != null): callable = callable.bindv(parameters) assert_called_count(callable, expected_count) ## @deprecated: no longer supported. func assert_setget( instance, name_property, const_or_setter = null, getter="__not_set__"): _lgr.deprecated('assert_property') _fail('assert_setget has been removed. Use assert_property, assert_set_property, assert_readonly_property instead.') # ---------------- #endregion # ############################################################################## #(G)odot (U)nit (T)est class # # ############################################################################## # The MIT License (MIT) # ===================== # # Copyright (c) 2025 Tom "Butch" Wesley # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ############################################################################## # View readme for usage details. # # Version - see gut.gd # ############################################################################## # Class that all test scripts must extend.` # # This provides all the asserts and other testing features. Test scripts are # run by the Gut class in gut.gd # ############################################################################## ================================================ FILE: demo/addons/gut/test.gd.uid ================================================ uid://cnup2nbj45r4s ================================================ FILE: demo/addons/gut/test_collector.gd ================================================ # ------------------------------------------------------------------------------ # This class handles calling out to the test parser and maintaining an array of # collected_script.gd. This is used for both calling the tests and tracking # the results of each script and test's execution. # # This also handles exporting and importing tests. # ------------------------------------------------------------------------------ var CollectedScript = GutUtils.CollectedScript var CollectedTest = GutUtils.CollectedTest var _test_prefix = 'test_' var _test_class_prefix = 'Test' var _lgr = GutUtils.get_logger() # Array of CollectedScripts. var scripts = [] func _does_inherit_from_test(thing): var base_script = thing.get_base_script() var to_return = false if(base_script != null): var base_path = base_script.get_path() if(base_path == 'res://addons/gut/test.gd'): to_return = true else: to_return = _does_inherit_from_test(base_script) return to_return func _populate_tests(test_script): var script = test_script.load_script() if(script == null): print(' !!! ', test_script.path, ' could not be loaded') return false test_script.is_loaded = true var methods = script.get_script_method_list() for i in range(methods.size()): var name = methods[i]['name'] if(name.begins_with(_test_prefix)): var t = CollectedTest.new() t.name = name t.arg_count = methods[i]['args'].size() test_script.tests.append(t) t.collected_script = weakref(test_script) func _get_inner_test_class_names(loaded): var inner_classes = [] var const_map = loaded.get_script_constant_map() for key in const_map: var thing = const_map[key] if(GutUtils.is_gdscript(thing)): if(key.begins_with(_test_class_prefix)): if(_does_inherit_from_test(thing)): inner_classes.append(key) else: _lgr.warn(str('Ignoring Inner Class ', key, ' because it does not extend GutTest')) # This could go deeper and find inner classes within inner classes # but requires more experimentation. Right now I'm keeping it at # one level since that is what the previous version did and there # has been no demand for deeper nesting. # _populate_inner_test_classes(thing) return inner_classes func _parse_script(test_script): var inner_classes = [] var scripts_found = [] var loaded = GutUtils.WarningsManager.load_script_using_custom_warnings( test_script.path, GutUtils.warnings_when_loading_test_scripts) if(_does_inherit_from_test(loaded)): _populate_tests(test_script) scripts_found.append(test_script.path) inner_classes = _get_inner_test_class_names(loaded) else: return [] for i in range(inner_classes.size()): var loaded_inner = loaded.get(inner_classes[i]) if(_does_inherit_from_test(loaded_inner)): var ts = CollectedScript.new(_lgr) ts.path = test_script.path ts.inner_class_name = inner_classes[i] _populate_tests(ts) scripts.append(ts) scripts_found.append(test_script.path + '[' + inner_classes[i] +']') return scripts_found # ----------------- # Public # ----------------- func add_script(path): # SHORTCIRCUIT if(has_script(path)): return [] # SHORTCIRCUIT if(!FileAccess.file_exists(path)): # This check was added so tests could create dynmaic scripts and add # them to be run through gut. This helps cut down on creating test # scripts to be used in test/resources. if(ResourceLoader.has_cached(path)): _lgr.debug("Using cached version of " + path) else: _lgr.error('Could not find script: ' + path) return var ts = CollectedScript.new(_lgr) ts.path = path # Append right away because if we don't test_doubler.gd.TestInitParameters # will HARD crash. I couldn't figure out what was causing the issue but # appending right away, and then removing if it's not valid seems to fix # things. It might have to do with the ordering of the test classes in # the test collecter. I'm not really sure. scripts.append(ts) var parse_results = _parse_script(ts) if(parse_results.find(path) == -1): _lgr.warn(str('Ignoring script ', path, ' because it does not extend GutTest')) scripts.remove_at(scripts.find(ts)) return parse_results func clear(): scripts.clear() func has_script(path): var found = false var idx = 0 while(idx < scripts.size() and !found): if(scripts[idx].get_full_name() == path): found = true else: idx += 1 return found func export_tests(path): var success = true var f = ConfigFile.new() for i in range(scripts.size()): scripts[i].export_to(f, str('CollectedScript-', i)) var result = f.save(path) if(result != OK): _lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result)) success = false return success func import_tests(path): var success = false var f = ConfigFile.new() var result = f.load(path) if(result != OK): _lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result)) else: var sections = f.get_sections() for key in sections: var ts = CollectedScript.new(_lgr) ts.import_from(f, key) _populate_tests(ts) scripts.append(ts) success = true return success func get_script_named(name): return GutUtils.search_array(scripts, 'get_filename_and_inner', name) func get_test_named(script_name, test_name): var s = get_script_named(script_name) if(s != null): return s.get_test_named(test_name) else: return null func to_s(): var to_return = '' for i in range(scripts.size()): to_return += scripts[i].to_s() + "\n" return to_return # --------------------- # Accessors # --------------------- func get_logger(): return _lgr func set_logger(logger): _lgr = logger func get_test_prefix(): return _test_prefix func set_test_prefix(test_prefix): _test_prefix = test_prefix func get_test_class_prefix(): return _test_class_prefix func set_test_class_prefix(test_class_prefix): _test_class_prefix = test_class_prefix func get_scripts(): return scripts func get_ran_test_count(): var count = 0 for s in scripts: count += s.get_ran_test_count() return count func get_ran_script_count(): var count = 0 for s in scripts: if(s.was_run): count += 1 return count func get_test_count(): var count = 0 for s in scripts: count += s.tests.size() return count func get_assert_count(): var count = 0 for s in scripts: count += s.get_assert_count() return count func get_pass_count(): var count = 0 for s in scripts: count += s.get_pass_count() return count func get_fail_count(): var count = 0 for s in scripts: count += s.get_fail_count() return count func get_pending_count(): var count = 0 for s in scripts: count += s.get_pending_count() return count ================================================ FILE: demo/addons/gut/test_collector.gd.uid ================================================ uid://cly8ws3u71jk5 ================================================ FILE: demo/addons/gut/thing_counter.gd ================================================ var things = {} func get_unique_count(): return things.size() func add_thing_to_count(thing): if(!things.has(thing)): things[thing] = 0 func add(thing): if(things.has(thing)): things[thing] += 1 else: things[thing] = 1 func has(thing): return things.has(thing) func count(thing): var to_return = 0 if(things.has(thing)): to_return = things[thing] return to_return func sum(): var to_return = 0 for key in things: to_return += things[key] return to_return func to_s(): var to_return = "" for key in things: to_return += str(key, ": ", things[key], "\n") to_return += str("sum: ", sum()) return to_return func get_max_count(): var max_val = null for key in things: if(max_val == null or things[key] > max_val): max_val = things[key] return max_val func add_array_items(array): for i in range(array.size()): add(array[i]) ================================================ FILE: demo/addons/gut/thing_counter.gd.uid ================================================ uid://8evk5cwvo2nu ================================================ FILE: demo/addons/gut/utils.gd ================================================ @tool class_name GutUtils extends Object const GUT_METADATA = '__gutdbl' # Note, these cannot change since places are checking for TYPE_INT to determine # how to process parameters. enum DOUBLE_STRATEGY{ INCLUDE_NATIVE, SCRIPT_ONLY, } enum DIFF { DEEP, SIMPLE } const TEST_STATUSES = { NO_ASSERTS = 'no asserts', SKIPPED = 'skipped', NOT_RUN = 'not run', PENDING = 'pending', # These two got the "ed" b/c pass is a reserved word and I could not # think of better words. FAILED = 'fail', PASSED = 'pass' } const DOUBLE_TEMPLATES = { FUNCTION = 'res://addons/gut/double_templates/function_template.txt', INIT = 'res://addons/gut/double_templates/init_template.txt', SCRIPT = 'res://addons/gut/double_templates/script_template.txt', } const NOTHING := '__NOTHING__' const NO_TEST := 'NONE' const GUT_ERROR_TYPE = 999 enum TREAT_AS { NOTHING, FAILURE, } ## This dictionary defaults to all the native classes that we cannot call new ## on. It is further populated during a run so that we only have to create ## a new instance once to get the class name string. static var gdscript_native_class_names_by_type = { Tween:"Tween" } static var GutScene = load('res://addons/gut/GutScene.tscn') static var LazyLoader = load('res://addons/gut/lazy_loader.gd') static var VersionNumbers = load("res://addons/gut/version_numbers.gd") static var WarningsManager = load("res://addons/gut/warnings_manager.gd") static var EditorGlobals = load("res://addons/gut/gui/editor_globals.gd") static var RunExternallyScene = load("res://addons/gut/gui/RunExternally.tscn") # -------------------------------- # Lazy loaded scripts. These scripts are lazy loaded so that they can be # declared, but will not load when this script is loaded. This gives us a # window at the start of a run to adjust warning levels prior to loading # everything. # -------------------------------- static var AutoFree = LazyLoader.new('res://addons/gut/autofree.gd'): get: return AutoFree.get_loaded() set(val): pass static var Awaiter = LazyLoader.new('res://addons/gut/awaiter.gd'): get: return Awaiter.get_loaded() set(val): pass static var Comparator = LazyLoader.new('res://addons/gut/comparator.gd'): get: return Comparator.get_loaded() set(val): pass static var CollectedTest = LazyLoader.new('res://addons/gut/collected_test.gd'): get: return CollectedTest.get_loaded() set(val): pass static var CollectedScript = LazyLoader.new('res://addons/gut/collected_script.gd'): get: return CollectedScript.get_loaded() set(val): pass static var CompareResult = LazyLoader.new('res://addons/gut/compare_result.gd'): get: return CompareResult.get_loaded() set(val): pass static var DiffFormatter = LazyLoader.new("res://addons/gut/diff_formatter.gd"): get: return DiffFormatter.get_loaded() set(val): pass static var DiffTool = LazyLoader.new('res://addons/gut/diff_tool.gd'): get: return DiffTool.get_loaded() set(val): pass static var DoubleTools = LazyLoader.new("res://addons/gut/double_tools.gd"): get: return DoubleTools.get_loader() set(val): pass static var Doubler = LazyLoader.new('res://addons/gut/doubler.gd'): get: return Doubler.get_loaded() set(val): pass static var DynamicGdScript = LazyLoader.new("res://addons/gut/dynamic_gdscript.gd") : get: return DynamicGdScript.get_loaded() set(val): pass static var Gut = LazyLoader.new('res://addons/gut/gut.gd'): get: return Gut.get_loaded() set(val): pass static var GutConfig = LazyLoader.new('res://addons/gut/gut_config.gd'): get: return GutConfig.get_loaded() set(val): pass static var GutFonts = LazyLoader.new("res://addons/gut/gut_fonts.gd"): get: return GutFonts.get_loaded() set(val): pass static var HookScript = LazyLoader.new('res://addons/gut/hook_script.gd'): get: return HookScript.get_loaded() set(val): pass static var InnerClassRegistry = LazyLoader.new('res://addons/gut/inner_class_registry.gd'): get: return InnerClassRegistry.get_loaded() set(val): pass static var InputFactory = LazyLoader.new("res://addons/gut/input_factory.gd"): get: return InputFactory.get_loaded() set(val): pass static var InputSender = LazyLoader.new("res://addons/gut/input_sender.gd"): get: return InputSender.get_loaded() set(val): pass static var JunitXmlExport = LazyLoader.new('res://addons/gut/junit_xml_export.gd'): get: return JunitXmlExport.get_loaded() set(val): pass static var GutLogger = LazyLoader.new('res://addons/gut/logger.gd') : # everything should use get_logger get: return GutLogger.get_loaded() set(val): pass static var MethodMaker = LazyLoader.new('res://addons/gut/method_maker.gd'): get: return MethodMaker.get_loaded() set(val): pass static var OneToMany = LazyLoader.new('res://addons/gut/one_to_many.gd'): get: return OneToMany.get_loaded() set(val): pass static var OptionMaker = LazyLoader.new('res://addons/gut/gui/option_maker.gd'): get: return OptionMaker.get_loaded() set(val): pass static var OrphanCounter = LazyLoader.new('res://addons/gut/orphan_counter.gd'): get: return OrphanCounter.get_loaded() set(val): pass static var ParameterFactory = LazyLoader.new('res://addons/gut/parameter_factory.gd'): get: return ParameterFactory.get_loaded() set(val): pass static var ParameterHandler = LazyLoader.new('res://addons/gut/parameter_handler.gd'): get: return ParameterHandler.get_loaded() set(val): pass static var Printers = LazyLoader.new('res://addons/gut/printers.gd'): get: return Printers.get_loaded() set(val): pass static var ResultExporter = LazyLoader.new('res://addons/gut/result_exporter.gd'): get: return ResultExporter.get_loaded() set(val): pass static var ScriptCollector = LazyLoader.new('res://addons/gut/script_parser.gd'): get: return ScriptCollector.get_loaded() set(val): pass static var SignalWatcher = LazyLoader.new('res://addons/gut/signal_watcher.gd'): get: return SignalWatcher.get_loaded() set(val): pass static var Spy = LazyLoader.new('res://addons/gut/spy.gd'): get: return Spy.get_loaded() set(val): pass static var Strutils = LazyLoader.new('res://addons/gut/strutils.gd'): get: return Strutils.get_loaded() set(val): pass static var Stubber = LazyLoader.new('res://addons/gut/stubber.gd'): get: return Stubber.get_loaded() set(val): pass static var StubParams = LazyLoader.new('res://addons/gut/stub_params.gd'): get: return StubParams.get_loaded() set(val): pass static var Summary = LazyLoader.new('res://addons/gut/summary.gd'): get: return Summary.get_loaded() set(val): pass static var Test = LazyLoader.new('res://addons/gut/test.gd'): get: return Test.get_loaded() set(val): pass static var TestCollector = LazyLoader.new('res://addons/gut/test_collector.gd'): get: return TestCollector.get_loaded() set(val): pass static var ThingCounter = LazyLoader.new('res://addons/gut/thing_counter.gd'): get: return ThingCounter.get_loaded() set(val): pass # -------------------------------- static var gut_fonts = GutFonts.new() static var avail_fonts = gut_fonts.get_font_names() static var version_numbers = VersionNumbers.new( # gut_versrion (source of truth) '9.5.0', # required_godot_version '4.5' ) static var warnings_at_start := { # WarningsManager dictionary exclude_addons = true } static var warnings_when_loading_test_scripts := { # WarningsManager dictionary enable = false } # ------------------------------------------------------------------------------ # Everything should get a logger through this. # # When running in test mode this will always return a new logger so that errors # are not caused by getting bad warn/error/etc counts. # ------------------------------------------------------------------------------ static var _lgr = null static func get_logger(): if(_lgr == null): _lgr = GutLogger.new() return _lgr static var _error_tracker = null static func get_error_tracker(): if(_error_tracker == null): _error_tracker = GutErrorTracker.new() return _error_tracker static var _dyn_gdscript = DynamicGdScript.new() static func create_script_from_source(source, override_path=null): var are_warnings_enabled = WarningsManager.are_warnings_enabled() WarningsManager.enable_warnings(false) var DynamicScript = _dyn_gdscript.create_script_from_source(source, override_path) if(typeof(DynamicScript) == TYPE_INT): var l = get_logger() l.error(str('Could not create script from source. Error: ', DynamicScript)) l.info(str("Source Code:\n", add_line_numbers(source))) WarningsManager.enable_warnings(are_warnings_enabled) return DynamicScript # Get the EditorInterface instance without having to make a direct reference to # it. This allows for testing to be done on editor scripts that require it # without having the parser error when you refer to it when not in the editor. static func get_editor_interface(): if(Engine.is_editor_hint()): var src = """ func get_it(): return EditorInterface """ var s = create_script_from_source(src).new() return s.get_it() else: return null static func godot_version_string(): return version_numbers.make_godot_version_string() static func is_godot_version(expected): return VersionNumbers.VerNumTools.is_godot_version_eq(expected) static func is_godot_version_gte(expected): return VersionNumbers.VerNumTools.is_godot_version_gte(expected) const INSTALL_OK_TEXT = 'Everything checks out' static func make_install_check_text(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): var text = INSTALL_OK_TEXT if(!FileAccess.file_exists(template_paths.FUNCTION) or !FileAccess.file_exists(template_paths.INIT) or !FileAccess.file_exists(template_paths.SCRIPT)): text = 'One or more GUT template files are missing. If this is an exported project, you must include *.txt files in the export to run GUT. If it is not an exported project then reinstall GUT.' elif(!ver_nums.is_godot_version_valid()): text = ver_nums.get_bad_version_text() return text static func is_install_valid(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): return make_install_check_text(template_paths, ver_nums) == INSTALL_OK_TEXT # ------------------------------------------------------------------------------ # Gets the root node without having to be in the tree and pushing out an error # if we don't have a main loop ready to go yet. # ------------------------------------------------------------------------------ # static func get_root_node(): # var main_loop = Engine.get_main_loop() # if(main_loop != null): # return main_loop.root # else: # push_error('No Main Loop Yet') # return null # ------------------------------------------------------------------------------ # Gets the value from an enum. # - If passed an integer value as a string it will convert it to an int and # processes the int value. # - If the value is a float then it is converted to an int and then processes # the int value # - If the value is an int, or was converted to an int, then the enum is checked # to see if it contains the value, if so then the value is returned. # Otherwise the default is returned. # - If the value is a string then it is uppercased and all spaces are replaced # with underscores. It then checks to see if enum contains a key of that # name. If so then the value for that key is returned, otherwise the default # is returned. # # This description is longer than the code, you should have just read the code # and the tests. # ------------------------------------------------------------------------------ static func get_enum_value(thing, e, default=null): var to_return = default if(typeof(thing) == TYPE_STRING and str(thing.to_int()) == thing): thing = thing.to_int() elif(typeof(thing) == TYPE_FLOAT): thing = int(thing) if(typeof(thing) == TYPE_STRING): var converted = thing.to_upper().replace(' ', '_') if(e.keys().has(converted)): to_return = e[converted] else: if(e.values().has(thing)): to_return = thing return to_return # ------------------------------------------------------------------------------ # return if_null if value is null otherwise return value # ------------------------------------------------------------------------------ static func nvl(value, if_null): if(value == null): return if_null else: return value # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ static func pretty_print(dict, indent = ' '): print(JSON.stringify(dict, indent)) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ static func print_properties(props, thing, print_all_meta=false): for i in range(props.size()): var prop_name = props[i].name var prop_value = thing.get(props[i].name) var print_value = str(prop_value) if(print_value.length() > 100): print_value = print_value.substr(0, 97) + '...' elif(print_value == ''): print_value = 'EMPTY' print(prop_name, ' = ', print_value) if(print_all_meta): print(' ', props[i]) static func print_method_list(thing): for entry in thing.get_method_list(): print("* ", entry.name) # ------------------------------------------------------------------------------ # Gets the value of the node_property 'script' from a PackedScene's root node. # This does not assume the location of the root node in the PackedScene's node # list. This also does not assume the index of the 'script' node property in # a nodes's property list. # ------------------------------------------------------------------------------ static func get_scene_script_object(scene): var state = scene.get_state() var to_return = null var root_node_path = NodePath(".") var node_idx = 0 while(node_idx < state.get_node_count() and to_return == null): if(state.get_node_path(node_idx) == root_node_path): for i in range(state.get_node_property_count(node_idx)): if(state.get_node_property_name(node_idx, i) == 'script'): to_return = state.get_node_property_value(node_idx, i) node_idx += 1 return to_return # ------------------------------------------------------------------------------ # returns true if the object has been freed, false if not # # From what i've read, the weakref approach should work. It seems to work most # of the time but sometimes it does not catch it. The str comparison seems to # fill in the gaps. I've not seen any errors after adding that check. # ------------------------------------------------------------------------------ static func is_freed(obj): var wr = weakref(obj) return !(wr.get_ref() and str(obj) != '') # ------------------------------------------------------------------------------ # Pretty self explanitory. # ------------------------------------------------------------------------------ static func is_not_freed(obj): return !is_freed(obj) # ------------------------------------------------------------------------------ # Checks if the passed in object is a GUT Double or Partial Double. # ------------------------------------------------------------------------------ static func is_double(obj): var to_return = false if(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)): to_return = obj.has_method('__gutdbl_check_method__') return to_return # ------------------------------------------------------------------------------ # Checks an object to see if it is a GDScriptNativeClass # ------------------------------------------------------------------------------ static func is_native_class(thing): var it_is = false if(typeof(thing) == TYPE_OBJECT): it_is = str(thing).begins_with("= 0): temp = decimal_value >> count if(temp & 1): binary_string = binary_string + "1" else: binary_string = binary_string + "0" count -= 1 return binary_string static func add_line_numbers(contents): if(contents == null): return '' var to_return = "" var lines = contents.split("\n") var line_num = 1 for line in lines: var line_str = str(line_num).lpad(6, ' ') to_return += str(line_str, ' |', line, "\n") line_num += 1 return to_return static func get_display_size(): return Engine.get_main_loop().get_viewport().get_visible_rect() static func find_method_meta(methods, method_name): var meta = null var idx = 0 while (idx < methods.size() and meta == null): var m = methods[idx] if(m.name == method_name): meta = m idx += 1 return meta static func get_method_meta(object, method_name): return find_method_meta(object.get_method_list(), 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/utils.gd.uid ================================================ uid://dbfbnvoq5osf2 ================================================ FILE: demo/addons/gut/version_conversion.gd ================================================ class ConfigurationUpdater: var EditorGlobals = load("res://addons/gut/gui/editor_globals.gd") func warn(message): print('GUT Warning: ', message) func info(message): print("GUT Info: ", message) func moved_file(from, to): if(FileAccess.file_exists(from) and !FileAccess.file_exists(to)): info(str('Copying [', from, '] to [', to, ']')) var result = DirAccess.copy_absolute(from, to) if(result != OK): warn(str('Could not copy [', from, '] to [', to, ']')) if(FileAccess.file_exists(from) and FileAccess.file_exists(to)): warn(str('File [', from, '] has been moved to [', to, "].\n You can delete ", from)) func move_user_file(from, to): if(from.begins_with('user://') and to.begins_with('user://')): if(FileAccess.file_exists(from) and !FileAccess.file_exists(to)): info(str('Moving [', from, '] to [', to, ']')) var result = DirAccess.copy_absolute(from, to) if(result == OK): info(str(' ', 'Created ', to)) result = DirAccess.remove_absolute(from) if(result != OK): warn(str(' ', 'Could not delete ', from)) else: info(str(' ', 'Deleted ', from)) else: warn(str(' ', 'Could not copy [', from, '] to [', to, ']')) else: warn(str('Attempt to move_user_file with files not in user:// ', from, '->', to)) func remove_user_file(which): if(which.begins_with('user://') and FileAccess.file_exists(which)): info(str('Deleting obsolete file ', which)) var result = DirAccess.remove_absolute(which) if(result != OK): warn(str(' ', 'Could not delete ', which)) else: info(str(' ', 'Deleted ', which)) class v9_2_0: extends ConfigurationUpdater func validate(): moved_file('res://.gut_editor_config.json', EditorGlobals.editor_run_gut_config_path) moved_file('res://.gut_editor_shortcuts.cfg', EditorGlobals.editor_shortcuts_path) remove_user_file('user://.gut_editor.bbcode') remove_user_file('user://.gut_editor.json') # list=Array[Dictionary]([{ # "base": &"RefCounted", # "class": &"DynamicGutTest", # "icon": "", # "language": &"GDScript", # "path": "res://test/resources/tools/dynamic_gut_test.gd" # }, { # "base": &"RefCounted", # "class": &"GutDoubleTestInnerClasses", # "icon": "", # "language": &"GDScript", # "path": "res://test/resources/doubler_test_objects/inner_classes.gd" # }, ... ]) static func get_missing_gut_class_names() -> Array: var gut_class_names = [ "GutErrorTracker", "GutHookScript", "GutInputFactory", "GutInputSender", "GutMain", "GutStringUtils", "GutTest", "GutTrackedError", "GutUtils", ] var class_cach_path = 'res://.godot/global_script_class_cache.cfg' var cfg = ConfigFile.new() cfg.load(class_cach_path) var all_class_names = {} var missing = [] var class_cache_entries = cfg.get_value('', 'list', []) for entry in class_cache_entries: if(entry.path.begins_with(&"res://addons/gut/")): # print(entry["class"], ': ', entry["path"]) all_class_names[entry["class"]] = entry for cn in gut_class_names: if(!all_class_names.has(cn)): missing.append(cn) return missing static func error_if_not_all_classes_imported() -> bool: var missing_class_names = get_missing_gut_class_names() if(missing_class_names.size() > 0): push_error(str("Some GUT class_names have not been imported. Please restart the Editor or run godot --headless --import\n", "Missing class_names: ", missing_class_names)) return true else: return false static func convert(): var inst = v9_2_0.new() inst.validate() ================================================ FILE: demo/addons/gut/version_conversion.gd.uid ================================================ uid://c8twdri50qrkb ================================================ FILE: demo/addons/gut/version_numbers.gd ================================================ # ############################################################################## # # ############################################################################## class VerNumTools: static func _make_version_array_from_string(v): var parts = Array(v.split('.')) for i in range(parts.size()): var int_val = parts[i].to_int() if(str(int_val) == parts[i]): parts[i] = parts[i].to_int() return parts static func make_version_array(v): var to_return = [] if(typeof(v) == TYPE_STRING): to_return = _make_version_array_from_string(v) elif(typeof(v) == TYPE_DICTIONARY): return [v.major, v.minor, v.patch] elif(typeof(v) == TYPE_ARRAY): to_return = v return to_return static func make_version_string(version_parts): var to_return = 'x.x.x' if(typeof(version_parts) == TYPE_ARRAY): to_return = ".".join(version_parts) elif(typeof(version_parts) == TYPE_DICTIONARY): to_return = str(version_parts.major, '.', version_parts.minor, '.', version_parts.patch) elif(typeof(version_parts) == TYPE_STRING): to_return = version_parts return to_return static func is_version_gte(version, required): var is_ok = null var v = make_version_array(version) var r = make_version_array(required) var idx = 0 while(is_ok == null and idx < v.size() and idx < r.size()): if(v[idx] > r[idx]): is_ok = true elif(v[idx] < r[idx]): is_ok = false idx += 1 # still null means each index was the same. return GutUtils.nvl(is_ok, true) static func is_version_eq(version, expected): var version_array = make_version_array(version) var expected_array = make_version_array(expected) if(expected_array.size() > version_array.size()): return false var is_version = true var i = 0 while(i < expected_array.size() and i < version_array.size() and is_version): if(expected_array[i] == version_array[i]): i += 1 else: is_version = false return is_version static func is_godot_version_eq(expected): return VerNumTools.is_version_eq(Engine.get_version_info(), expected) static func is_godot_version_gte(expected): return VerNumTools.is_version_gte(Engine.get_version_info(), expected) # ############################################################################## # # ############################################################################## var gut_version = '0.0.0' var required_godot_version = '0.0.0' func _init(gut_v = gut_version, required_godot_v = required_godot_version): gut_version = gut_v required_godot_version = required_godot_v # ------------------------------------------------------------------------------ # Blurb of text with GUT and Godot versions. # ------------------------------------------------------------------------------ func get_version_text(): var v_info = Engine.get_version_info() var gut_version_info = str('GUT version: ', gut_version) var godot_version_info = str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch) return godot_version_info + "\n" + gut_version_info # ------------------------------------------------------------------------------ # Returns a nice string for erroring out when we have a bad Godot version. # ------------------------------------------------------------------------------ func get_bad_version_text(): var info = Engine.get_version_info() var gd_version = str(info.major, '.', info.minor, '.', info.patch) return 'GUT ' + gut_version + ' requires Godot ' + required_godot_version + \ ' or greater. Godot version is ' + gd_version # ------------------------------------------------------------------------------ # Checks the Godot version against required_godot_version. # ------------------------------------------------------------------------------ func is_godot_version_valid(): return VerNumTools.is_version_gte(Engine.get_version_info(), required_godot_version) func make_godot_version_string(): return VerNumTools.make_version_string(Engine.get_version_info()) ================================================ FILE: demo/addons/gut/version_numbers.gd.uid ================================================ uid://b4bb6lchs5uba ================================================ FILE: demo/addons/gut/warnings_manager.gd ================================================ const IGNORE = 0 const WARN = 1 const ERROR = 2 const WARNING_LOOKUP = { IGNORE : 'IGNORE', WARN : 'WARN', ERROR : 'ERROR' } const GDSCRIPT_WARNING = 'debug/gdscript/warnings/' # --------------------------------------- # Static # --------------------------------------- static var _static_init_called = false # This is static and set in _static_init so that we can get the current settings as # soon as possible. static var _project_warnings : Dictionary = {} static var _disabled = false # should never be true, unless it is, but it shouldn't be. Whatever it is, it # should stay the same for the entire run. Read only. static var disabled = _disabled: get: return _disabled set(val):pass static var project_warnings := {} : get: # somehow this gets called before _project_warnings is initialized when # loading a project in the editor. It causes an error stating that # duplicate can't be called on nil. It seems there might be an # implicit "get" call happening. Using push_error I saw a message # in this method, but not one from _static_init upon loading the project if(_static_init_called): return _project_warnings.duplicate() else: return {} set(val): pass static func _static_init(): _project_warnings = create_warnings_dictionary_from_project_settings() _static_init_called = true if(disabled): print(""" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! Warnings Manager has been disabled !! !! Do not push this up buddy !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! """.dedent()) static func are_warnings_enabled(): return ProjectSettings.get(str(GDSCRIPT_WARNING, 'enable')) ## Turn all warnings on/off. Use reset_warnings to restore the original value. static func enable_warnings(should=true): if(disabled): return ProjectSettings.set(str(GDSCRIPT_WARNING, 'enable'), should) ## Turn on/off excluding addons. Use reset_warnings to restore the original value. static func exclude_addons(should=true): if(disabled): return ProjectSettings.set(str(GDSCRIPT_WARNING, 'exclude_addons'), should) ## Resets warning settings to what they are set to in Project Settings static func reset_warnings(): apply_warnings_dictionary(_project_warnings) static func set_project_setting_warning(warning_name : String, value : Variant): if(disabled): return var property_name = str(GDSCRIPT_WARNING, warning_name) # This check will generate a warning if the setting does not exist if(property_name in ProjectSettings): ProjectSettings.set(property_name, value) static func apply_warnings_dictionary(warning_values : Dictionary): if(disabled): return for key in warning_values: set_project_setting_warning(key, warning_values[key]) static func create_ignore_all_dictionary(): return replace_warnings_values(project_warnings, -1, IGNORE) static func create_warn_all_warnings_dictionary(): return replace_warnings_values(project_warnings, -1, WARN) static func replace_warnings_with_ignore(dict): return replace_warnings_values(dict, WARN, IGNORE) static func replace_errors_with_warnings(dict): return replace_warnings_values(dict, ERROR, WARN) static func replace_warnings_values(dict, replace_this, with_this): var to_return = dict.duplicate() for key in to_return: if(typeof(to_return[key]) == TYPE_INT and (replace_this == -1 or to_return[key] == replace_this)): to_return[key] = with_this return to_return static func create_warnings_dictionary_from_project_settings() -> Dictionary : var props = ProjectSettings.get_property_list() var to_return = {} for i in props.size(): if(props[i].name.begins_with(GDSCRIPT_WARNING)): var prop_name = props[i].name.replace(GDSCRIPT_WARNING, '') to_return[prop_name] = ProjectSettings.get(props[i].name) return to_return static func print_warnings_dictionary(which : Dictionary): var is_valid = true for key in which: var value_str = str(which[key]) if(_project_warnings.has(key)): if(typeof(which[key]) == TYPE_INT): if(WARNING_LOOKUP.has(which[key])): value_str = WARNING_LOOKUP[which[key]] else: push_warning(str(which[key], ' is not a valid value for ', key)) is_valid = false else: push_warning(str(key, ' is not a valid warning setting')) is_valid = false var s = str(key, ' = ', value_str) print(s) return is_valid static func load_script_ignoring_all_warnings(path : String) -> Variant: return load_script_using_custom_warnings(path, create_ignore_all_dictionary()) static func load_script_using_custom_warnings(path : String, warnings_dictionary : Dictionary) -> Variant: var current_warns = create_warnings_dictionary_from_project_settings() apply_warnings_dictionary(warnings_dictionary) var s = load(path) apply_warnings_dictionary(current_warns) return s ================================================ FILE: demo/addons/gut/warnings_manager.gd.uid ================================================ uid://blo71surxlb13 ================================================ FILE: demo/appstore.png.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://c47slho6dd1u2" path="res://.godot/imported/appstore.png-0b2f85bf34c98898c164059732fb30dc.ctex" metadata={ "vram_texture": false } [deps] source_file="res://appstore.png" dest_files=["res://.godot/imported/appstore.png-0b2f85bf34c98898c164059732fb30dc.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/assets/Banks/SFX.bank ================================================ [File too large to display: 20.6 MB] ================================================ FILE: demo/assets/Music/License.txt ================================================ Music Jingles by Kenney Vleugels (Kenney.nl) ------------------------------ License (Creative Commons Zero, CC0) http://creativecommons.org/publicdomain/zero/1.0/ You may use these assets in personal and commercial projects. Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. ------------------------------ Donate: http://support.kenney.nl Request: http://request.kenney.nl Follow on Twitter for updates: @KenneyNL ================================================ FILE: demo/assets/Music/jingles_SAX07.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bbg1iwsy4av7p" path="res://.godot/imported/jingles_SAX07.ogg-e0a2e38d4fb098d3cc5be1127eac0de1.oggvorbisstr" [deps] source_file="res://assets/Music/jingles_SAX07.ogg" dest_files=["res://.godot/imported/jingles_SAX07.ogg-e0a2e38d4fb098d3cc5be1127eac0de1.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/beltHandle1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dydllsb4naikb" path="res://.godot/imported/beltHandle1.ogg-5b80474bde850e2e07979e0b9dd05a40.oggvorbisstr" [deps] source_file="res://assets/Sounds/beltHandle1.ogg" dest_files=["res://.godot/imported/beltHandle1.ogg-5b80474bde850e2e07979e0b9dd05a40.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/beltHandle2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://ctatqi5eacvti" path="res://.godot/imported/beltHandle2.ogg-b75cd2508ee0ee251254411db27d1f2e.oggvorbisstr" [deps] source_file="res://assets/Sounds/beltHandle2.ogg" dest_files=["res://.godot/imported/beltHandle2.ogg-b75cd2508ee0ee251254411db27d1f2e.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookClose.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://d2v8bwyvepp4g" path="res://.godot/imported/bookClose.ogg-fa4b91a05f6232aedfc7b0b0f769ace8.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookClose.ogg" dest_files=["res://.godot/imported/bookClose.ogg-fa4b91a05f6232aedfc7b0b0f769ace8.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookFlip1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cyye74esp88lu" path="res://.godot/imported/bookFlip1.ogg-c00a802f57276e4c154c6490a1adfdb1.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookFlip1.ogg" dest_files=["res://.godot/imported/bookFlip1.ogg-c00a802f57276e4c154c6490a1adfdb1.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookFlip2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://biitt5nu0fx5o" path="res://.godot/imported/bookFlip2.ogg-844d4ca357872e8d6f285274de218590.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookFlip2.ogg" dest_files=["res://.godot/imported/bookFlip2.ogg-844d4ca357872e8d6f285274de218590.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookFlip3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cnulfritsm0sb" path="res://.godot/imported/bookFlip3.ogg-dc8e2bfa9dc330e8376e1b2fbbe16170.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookFlip3.ogg" dest_files=["res://.godot/imported/bookFlip3.ogg-dc8e2bfa9dc330e8376e1b2fbbe16170.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookOpen.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dbqp1pg2g5pmk" path="res://.godot/imported/bookOpen.ogg-f5eca4dcf9579626efc8d4cd81bac2c8.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookOpen.ogg" dest_files=["res://.godot/imported/bookOpen.ogg-f5eca4dcf9579626efc8d4cd81bac2c8.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookPlace1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cgq1hn6e7xc3w" path="res://.godot/imported/bookPlace1.ogg-9473a111ac56c964e35a3ce405643e88.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookPlace1.ogg" dest_files=["res://.godot/imported/bookPlace1.ogg-9473a111ac56c964e35a3ce405643e88.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookPlace2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://c4c36a70ro865" path="res://.godot/imported/bookPlace2.ogg-390a4879e48e2dcfd973c3e012f3b9b8.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookPlace2.ogg" dest_files=["res://.godot/imported/bookPlace2.ogg-390a4879e48e2dcfd973c3e012f3b9b8.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/bookPlace3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bhyb2rc3qk5ls" path="res://.godot/imported/bookPlace3.ogg-83530db2570a1d448e09955cb28b7c52.oggvorbisstr" [deps] source_file="res://assets/Sounds/bookPlace3.ogg" dest_files=["res://.godot/imported/bookPlace3.ogg-83530db2570a1d448e09955cb28b7c52.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/chop.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://xp6pojfjt6qq" path="res://.godot/imported/chop.ogg-c6363f08403778fa8c2db7513103fe64.oggvorbisstr" [deps] source_file="res://assets/Sounds/chop.ogg" dest_files=["res://.godot/imported/chop.ogg-c6363f08403778fa8c2db7513103fe64.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/cloth1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://drikcdbtu6kga" path="res://.godot/imported/cloth1.ogg-454aba1a0a43c6224cc67f1fa4c3d27a.oggvorbisstr" [deps] source_file="res://assets/Sounds/cloth1.ogg" dest_files=["res://.godot/imported/cloth1.ogg-454aba1a0a43c6224cc67f1fa4c3d27a.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/cloth2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dthttp8pdmayq" path="res://.godot/imported/cloth2.ogg-bae99958d207b4e17f9cd7a91b36afdf.oggvorbisstr" [deps] source_file="res://assets/Sounds/cloth2.ogg" dest_files=["res://.godot/imported/cloth2.ogg-bae99958d207b4e17f9cd7a91b36afdf.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/cloth3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://du5tovx03lxt6" path="res://.godot/imported/cloth3.ogg-a4237326ad50d528b3965cb7b78c88c6.oggvorbisstr" [deps] source_file="res://assets/Sounds/cloth3.ogg" dest_files=["res://.godot/imported/cloth3.ogg-a4237326ad50d528b3965cb7b78c88c6.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/cloth4.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bfq5cd33xqsiq" path="res://.godot/imported/cloth4.ogg-ce7ca0df36c75e48c93a562266b170ee.oggvorbisstr" [deps] source_file="res://assets/Sounds/cloth4.ogg" dest_files=["res://.godot/imported/cloth4.ogg-ce7ca0df36c75e48c93a562266b170ee.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/clothBelt.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://b0jvof842h4df" path="res://.godot/imported/clothBelt.ogg-8ad677919ef512f9ae399d549c502b58.oggvorbisstr" [deps] source_file="res://assets/Sounds/clothBelt.ogg" dest_files=["res://.godot/imported/clothBelt.ogg-8ad677919ef512f9ae399d549c502b58.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/clothBelt2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://8uwk7rtghtbb" path="res://.godot/imported/clothBelt2.ogg-f63060ada3e32df8afab079abd87beb5.oggvorbisstr" [deps] source_file="res://assets/Sounds/clothBelt2.ogg" dest_files=["res://.godot/imported/clothBelt2.ogg-f63060ada3e32df8afab079abd87beb5.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/creak1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bojslx0dun6se" path="res://.godot/imported/creak1.ogg-db9b54f10b9fa283cdc1e795aa365995.oggvorbisstr" [deps] source_file="res://assets/Sounds/creak1.ogg" dest_files=["res://.godot/imported/creak1.ogg-db9b54f10b9fa283cdc1e795aa365995.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/creak2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bd61n3r2mj32l" path="res://.godot/imported/creak2.ogg-a930fc882a2b6e71a0e476f77428f4c1.oggvorbisstr" [deps] source_file="res://assets/Sounds/creak2.ogg" dest_files=["res://.godot/imported/creak2.ogg-a930fc882a2b6e71a0e476f77428f4c1.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/creak3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dfdu8w2hbcild" path="res://.godot/imported/creak3.ogg-d1dd170afbc09d20efb8b28b7994904d.oggvorbisstr" [deps] source_file="res://assets/Sounds/creak3.ogg" dest_files=["res://.godot/imported/creak3.ogg-d1dd170afbc09d20efb8b28b7994904d.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorClose_1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://uwhyij31kf6x" path="res://.godot/imported/doorClose_1.ogg-fb2bd4d8c58de96b58c5a92f8db46991.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorClose_1.ogg" dest_files=["res://.godot/imported/doorClose_1.ogg-fb2bd4d8c58de96b58c5a92f8db46991.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorClose_2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bqoig28844rx0" path="res://.godot/imported/doorClose_2.ogg-fb315789363270c69acf8b9a7b168a86.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorClose_2.ogg" dest_files=["res://.godot/imported/doorClose_2.ogg-fb315789363270c69acf8b9a7b168a86.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorClose_3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bwuvku8j6rv23" path="res://.godot/imported/doorClose_3.ogg-56114bb9e0a5e61869880908f3b5a8a5.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorClose_3.ogg" dest_files=["res://.godot/imported/doorClose_3.ogg-56114bb9e0a5e61869880908f3b5a8a5.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorClose_4.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dbkktxrm5c4y8" path="res://.godot/imported/doorClose_4.ogg-1bb649f6ab99be6253ac5224baf07103.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorClose_4.ogg" dest_files=["res://.godot/imported/doorClose_4.ogg-1bb649f6ab99be6253ac5224baf07103.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorOpen_1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cpi37teqbwopp" path="res://.godot/imported/doorOpen_1.ogg-2cc2988618fb7d031150927eb359efc9.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorOpen_1.ogg" dest_files=["res://.godot/imported/doorOpen_1.ogg-2cc2988618fb7d031150927eb359efc9.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/doorOpen_2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dsmaot3lqm5ms" path="res://.godot/imported/doorOpen_2.ogg-4916d9512b4ac3e4ba88acf90a1f5af9.oggvorbisstr" [deps] source_file="res://assets/Sounds/doorOpen_2.ogg" dest_files=["res://.godot/imported/doorOpen_2.ogg-4916d9512b4ac3e4ba88acf90a1f5af9.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/drawKnife1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dw2txvwv5bub6" path="res://.godot/imported/drawKnife1.ogg-cd7700ce71ea897983a04459f74736fd.oggvorbisstr" [deps] source_file="res://assets/Sounds/drawKnife1.ogg" dest_files=["res://.godot/imported/drawKnife1.ogg-cd7700ce71ea897983a04459f74736fd.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/drawKnife2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dcssqqmaqgwql" path="res://.godot/imported/drawKnife2.ogg-3277966fa0ccf19387948aca56c2f148.oggvorbisstr" [deps] source_file="res://assets/Sounds/drawKnife2.ogg" dest_files=["res://.godot/imported/drawKnife2.ogg-3277966fa0ccf19387948aca56c2f148.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/drawKnife3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dvxp23851ib52" path="res://.godot/imported/drawKnife3.ogg-7ff84ee2dc7dd20ed24d01544c56cdc6.oggvorbisstr" [deps] source_file="res://assets/Sounds/drawKnife3.ogg" dest_files=["res://.godot/imported/drawKnife3.ogg-7ff84ee2dc7dd20ed24d01544c56cdc6.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/dropLeather.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dfou14surgda2" path="res://.godot/imported/dropLeather.ogg-f379e54b79e4375d25e07e40c5f14d56.oggvorbisstr" [deps] source_file="res://assets/Sounds/dropLeather.ogg" dest_files=["res://.godot/imported/dropLeather.ogg-f379e54b79e4375d25e07e40c5f14d56.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep00.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://b0y64o7e0p22i" path="res://.godot/imported/footstep00.ogg-c8328f3a28a4cf747f25018e77b22788.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep00.ogg" dest_files=["res://.godot/imported/footstep00.ogg-c8328f3a28a4cf747f25018e77b22788.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep01.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cpyem21efo8gc" path="res://.godot/imported/footstep01.ogg-bd79989daf496e93c5d8254c3f5e99a1.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep01.ogg" dest_files=["res://.godot/imported/footstep01.ogg-bd79989daf496e93c5d8254c3f5e99a1.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep02.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://1nomeut88u4q" path="res://.godot/imported/footstep02.ogg-304bdc4ea220c3fdd746a1fe8a27790f.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep02.ogg" dest_files=["res://.godot/imported/footstep02.ogg-304bdc4ea220c3fdd746a1fe8a27790f.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep03.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://becp8wddafyp4" path="res://.godot/imported/footstep03.ogg-16361b106b1414b04013abe70392756d.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep03.ogg" dest_files=["res://.godot/imported/footstep03.ogg-16361b106b1414b04013abe70392756d.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep04.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cn1ca7ilgbxg5" path="res://.godot/imported/footstep04.ogg-fa0eeefab9b4265ea9c83433c7eff9e6.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep04.ogg" dest_files=["res://.godot/imported/footstep04.ogg-fa0eeefab9b4265ea9c83433c7eff9e6.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep05.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dfpimayyis68b" path="res://.godot/imported/footstep05.ogg-6fd3eb14d963635ff2f473f45c6f2314.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep05.ogg" dest_files=["res://.godot/imported/footstep05.ogg-6fd3eb14d963635ff2f473f45c6f2314.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep06.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://odkpye6s6omw" path="res://.godot/imported/footstep06.ogg-d73a658a91f3fb7f652648268887f089.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep06.ogg" dest_files=["res://.godot/imported/footstep06.ogg-d73a658a91f3fb7f652648268887f089.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep07.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://baq0coor6bscw" path="res://.godot/imported/footstep07.ogg-26ceb5e06f5c062627ce1a0156ac6645.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep07.ogg" dest_files=["res://.godot/imported/footstep07.ogg-26ceb5e06f5c062627ce1a0156ac6645.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep08.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://b2ox2fswnhq55" path="res://.godot/imported/footstep08.ogg-ee19c27dab4fdf314dd8dd907273e7f6.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep08.ogg" dest_files=["res://.godot/imported/footstep08.ogg-ee19c27dab4fdf314dd8dd907273e7f6.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/footstep09.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://il238x71e0be" path="res://.godot/imported/footstep09.ogg-090ea3d9a9cc50cd7dc37e43721f4314.oggvorbisstr" [deps] source_file="res://assets/Sounds/footstep09.ogg" dest_files=["res://.godot/imported/footstep09.ogg-090ea3d9a9cc50cd7dc37e43721f4314.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/handleCoins.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cnca8pdcea734" path="res://.godot/imported/handleCoins.ogg-899543da392ce60fc37dd93aec297bb9.oggvorbisstr" [deps] source_file="res://assets/Sounds/handleCoins.ogg" dest_files=["res://.godot/imported/handleCoins.ogg-899543da392ce60fc37dd93aec297bb9.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/handleCoins2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bosfwq1n8hi6t" path="res://.godot/imported/handleCoins2.ogg-3014b56123531c3d044358fd5904b82c.oggvorbisstr" [deps] source_file="res://assets/Sounds/handleCoins2.ogg" dest_files=["res://.godot/imported/handleCoins2.ogg-3014b56123531c3d044358fd5904b82c.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/handleSmallLeather.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dxbl6dnn55gcy" path="res://.godot/imported/handleSmallLeather.ogg-43b67dc9c38260d94781822f1244d69f.oggvorbisstr" [deps] source_file="res://assets/Sounds/handleSmallLeather.ogg" dest_files=["res://.godot/imported/handleSmallLeather.ogg-43b67dc9c38260d94781822f1244d69f.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/handleSmallLeather2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cu6tlq87dqej" path="res://.godot/imported/handleSmallLeather2.ogg-fbb7df5a80d10bfc58e6acbea84f4ab5.oggvorbisstr" [deps] source_file="res://assets/Sounds/handleSmallLeather2.ogg" dest_files=["res://.godot/imported/handleSmallLeather2.ogg-fbb7df5a80d10bfc58e6acbea84f4ab5.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/knifeSlice.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://cxoy0f8kislgh" path="res://.godot/imported/knifeSlice.ogg-ff3fc6d1fe55494d8e63ac86ff57a090.oggvorbisstr" [deps] source_file="res://assets/Sounds/knifeSlice.ogg" dest_files=["res://.godot/imported/knifeSlice.ogg-ff3fc6d1fe55494d8e63ac86ff57a090.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/knifeSlice2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://bunyclrcy6qd4" path="res://.godot/imported/knifeSlice2.ogg-b0fec775bac0402f4d8d441fc72e7222.oggvorbisstr" [deps] source_file="res://assets/Sounds/knifeSlice2.ogg" dest_files=["res://.godot/imported/knifeSlice2.ogg-b0fec775bac0402f4d8d441fc72e7222.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/licence.txt ================================================ Sound file from Kenney Game assets https://www.kenney.nl/ License: (CC0 1.0 Universal) You're free to use these game assets in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but is greatly appreciated! ================================================ FILE: demo/assets/Sounds/metalClick.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://clb0yb3eitaf0" path="res://.godot/imported/metalClick.ogg-04388905a2f91e693ddc2a0f2dc0b57a.oggvorbisstr" [deps] source_file="res://assets/Sounds/metalClick.ogg" dest_files=["res://.godot/imported/metalClick.ogg-04388905a2f91e693ddc2a0f2dc0b57a.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/metalLatch.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://dmuixaikstwpw" path="res://.godot/imported/metalLatch.ogg-eeafac9c0415bd988baefc04749b2e94.oggvorbisstr" [deps] source_file="res://assets/Sounds/metalLatch.ogg" dest_files=["res://.godot/imported/metalLatch.ogg-eeafac9c0415bd988baefc04749b2e94.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/metalPot1.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://c2ye8r4j133h3" path="res://.godot/imported/metalPot1.ogg-1b5f6d48fae741734b835d833696a611.oggvorbisstr" [deps] source_file="res://assets/Sounds/metalPot1.ogg" dest_files=["res://.godot/imported/metalPot1.ogg-1b5f6d48fae741734b835d833696a611.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/metalPot2.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://d38uhbft8cy1f" path="res://.godot/imported/metalPot2.ogg-5a8d786b01c4367a67ce1185cbcba3c9.oggvorbisstr" [deps] source_file="res://assets/Sounds/metalPot2.ogg" dest_files=["res://.godot/imported/metalPot2.ogg-5a8d786b01c4367a67ce1185cbcba3c9.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/assets/Sounds/metalPot3.ogg.import ================================================ [remap] importer="oggvorbisstr" type="AudioStreamOggVorbis" uid="uid://lnvtp5i7yk4f" path="res://.godot/imported/metalPot3.ogg-77b1a58051f91d0ebee634bce4a5e0e3.oggvorbisstr" [deps] source_file="res://assets/Sounds/metalPot3.ogg" dest_files=["res://.godot/imported/metalPot3.ogg-77b1a58051f91d0ebee634bce4a5e0e3.oggvorbisstr"] [params] loop=false loop_offset=0 bpm=0 beat_count=0 bar_beats=4 ================================================ FILE: demo/default_env.tres ================================================ [gd_resource type="Environment" load_steps=2 format=2] [sub_resource type="Sky" id=1] [resource] background_mode = 2 background_sky = SubResource( 1 ) ================================================ FILE: demo/export_presets.cfg ================================================ [preset.0] name="Android" platform="Android" runnable=true dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" export_path="../../../../FMOD Gdnative Integration Demo.apk" encryption_include_filters="" encryption_exclude_filters="" encrypt_pck=false encrypt_directory=false [preset.0.options] custom_template/debug="" custom_template/release="" gradle_build/use_gradle_build=true gradle_build/export_format=0 gradle_build/min_sdk="" gradle_build/target_sdk="" plugins/FmodPlugin=true architectures/armeabi-v7a=false architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false version/code=1 version/name="1.0" package/unique_name="com.utopiarise" package/name="fmodgdnative" package/signed=true package/app_category=2 package/retain_data_on_uninstall=false package/exclude_from_recents=false launcher_icons/main_192x192="" launcher_icons/adaptive_foreground_432x432="" launcher_icons/adaptive_background_432x432="" graphics/opengl_debug=false xr_features/xr_mode=0 xr_features/hand_tracking=0 xr_features/hand_tracking_frequency=0 xr_features/passthrough=0 screen/immersive_mode=true screen/support_small=true screen/support_normal=true screen/support_large=true screen/support_xlarge=true user_data_backup/allow=false command_line/extra_args="" apk_expansion/enable=false apk_expansion/SALT="" apk_expansion/public_key="" permissions/custom_permissions=PackedStringArray() permissions/access_checkin_properties=false permissions/access_coarse_location=false permissions/access_fine_location=false permissions/access_location_extra_commands=false permissions/access_mock_location=false permissions/access_network_state=false permissions/access_surface_flinger=false permissions/access_wifi_state=false permissions/account_manager=false permissions/add_voicemail=false permissions/authenticate_accounts=false permissions/battery_stats=false permissions/bind_accessibility_service=false permissions/bind_appwidget=false permissions/bind_device_admin=false permissions/bind_input_method=false permissions/bind_nfc_service=false permissions/bind_notification_listener_service=false permissions/bind_print_service=false permissions/bind_remoteviews=false permissions/bind_text_service=false permissions/bind_vpn_service=false permissions/bind_wallpaper=false permissions/bluetooth=false permissions/bluetooth_admin=false permissions/bluetooth_privileged=false permissions/brick=false permissions/broadcast_package_removed=false permissions/broadcast_sms=false permissions/broadcast_sticky=false permissions/broadcast_wap_push=false permissions/call_phone=false permissions/call_privileged=false permissions/camera=false permissions/capture_audio_output=false permissions/capture_secure_video_output=false permissions/capture_video_output=false permissions/change_component_enabled_state=false permissions/change_configuration=false permissions/change_network_state=false permissions/change_wifi_multicast_state=false permissions/change_wifi_state=false permissions/clear_app_cache=false permissions/clear_app_user_data=false permissions/control_location_updates=false permissions/delete_cache_files=false permissions/delete_packages=false permissions/device_power=false permissions/diagnostic=false permissions/disable_keyguard=false permissions/dump=false permissions/expand_status_bar=false permissions/factory_test=false permissions/flashlight=false permissions/force_back=false permissions/get_accounts=false permissions/get_package_size=false permissions/get_tasks=false permissions/get_top_activity_info=false permissions/global_search=false permissions/hardware_test=false permissions/inject_events=false permissions/install_location_provider=false permissions/install_packages=false permissions/install_shortcut=false permissions/internal_system_window=false permissions/internet=false permissions/kill_background_processes=false permissions/location_hardware=false permissions/manage_accounts=false permissions/manage_app_tokens=false permissions/manage_documents=false permissions/manage_external_storage=false permissions/master_clear=false permissions/media_content_control=false permissions/modify_audio_settings=false permissions/modify_phone_state=false permissions/mount_format_filesystems=false permissions/mount_unmount_filesystems=false permissions/nfc=false permissions/persistent_activity=false permissions/process_outgoing_calls=false permissions/read_calendar=false permissions/read_call_log=false permissions/read_contacts=false permissions/read_external_storage=false permissions/read_frame_buffer=false permissions/read_history_bookmarks=false permissions/read_input_state=false permissions/read_logs=false permissions/read_phone_state=false permissions/read_profile=false permissions/read_sms=false permissions/read_social_stream=false permissions/read_sync_settings=false permissions/read_sync_stats=false permissions/read_user_dictionary=false permissions/reboot=false permissions/receive_boot_completed=false permissions/receive_mms=false permissions/receive_sms=false permissions/receive_wap_push=false permissions/record_audio=false permissions/reorder_tasks=false permissions/restart_packages=false permissions/send_respond_via_message=false permissions/send_sms=false permissions/set_activity_watcher=false permissions/set_alarm=false permissions/set_always_finish=false permissions/set_animation_scale=false permissions/set_debug_app=false permissions/set_orientation=false permissions/set_pointer_speed=false permissions/set_preferred_applications=false permissions/set_process_limit=false permissions/set_time=false permissions/set_time_zone=false permissions/set_wallpaper=false permissions/set_wallpaper_hints=false permissions/signal_persistent_processes=false permissions/status_bar=false permissions/subscribed_feeds_read=false permissions/subscribed_feeds_write=false permissions/system_alert_window=false permissions/transmit_ir=false permissions/uninstall_shortcut=false permissions/update_device_stats=false permissions/use_credentials=false permissions/use_sip=false permissions/vibrate=false permissions/wake_lock=false permissions/write_apn_settings=false permissions/write_calendar=false permissions/write_call_log=false permissions/write_contacts=false permissions/write_external_storage=false permissions/write_gservices=false permissions/write_history_bookmarks=false permissions/write_profile=false permissions/write_secure_settings=false permissions/write_settings=false permissions/write_sms=false permissions/write_social_stream=false permissions/write_sync_settings=false permissions/write_user_dictionary=false graphics/32_bits_framebuffer=true xr_features/degrees_of_freedom=0 one_click_deploy/clear_previous_install=false custom_template/use_custom_build=true screen/orientation=0 screen/opengl_debug=false [preset.1] name="Windows Desktop" platform="Windows Desktop" runnable=true dedicated_server=false custom_features="" export_filter="all_resources" include_filter="*.bank, *.ogg" exclude_filter="" export_path="../../../GodotFmodExport/FMOD.exe" encryption_include_filters="" encryption_exclude_filters="" encrypt_pck=false encrypt_directory=false [preset.1.options] custom_template/debug="" custom_template/release="" debug/export_console_wrapper=1 binary_format/embed_pck=false texture_format/bptc=false texture_format/s3tc=true texture_format/etc=false texture_format/etc2=false binary_format/architecture="x86_64" codesign/enable=false codesign/timestamp=true codesign/timestamp_server_url="" codesign/digest_algorithm=1 codesign/description="" codesign/custom_options=PackedStringArray() application/modify_resources=true application/icon="" application/console_wrapper_icon="" application/icon_interpolation=4 application/file_version="" application/product_version="" application/company_name="" application/product_name="" application/file_description="" application/copyright="" application/trademarks="" ssh_remote_deploy/enabled=false ssh_remote_deploy/host="user@host_ip" ssh_remote_deploy/port="22" ssh_remote_deploy/extra_args_ssh="" ssh_remote_deploy/extra_args_scp="" ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' $action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' $trigger = New-ScheduledTaskTrigger -Once -At 00:00 $settings = New-ScheduledTaskSettingsSet $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true Start-ScheduledTask -TaskName godot_remote_debug while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue Remove-Item -Recurse -Force '{temp_dir}'" texture_format/no_bptc_fallbacks=true binary_format/64_bits=true [preset.2] name="iOS" platform="iOS" runnable=true dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" export_path="../../../../Desktop/fmod-ios-export/FmodTestDemo.ipa" encryption_include_filters="" encryption_exclude_filters="" encrypt_pck=false encrypt_directory=false [preset.2.options] custom_template/debug="" custom_template/release="" architectures/arm64=true application/app_store_team_id="changeit" application/code_sign_identity_debug="iPhone Developer" application/export_method_debug=1 application/code_sign_identity_release="iPhone Developer" application/export_method_release=1 application/targeted_device_family=2 application/bundle_identifier="com.utopiarise.godot.fmod" application/signature="????" application/short_version="1.0" application/version="1.0" application/icon_interpolation=4 application/launch_screens_interpolation=4 capabilities/access_wifi=false capabilities/push_notifications=false user_data/accessible_from_files_app=false user_data/accessible_from_itunes_sharing=false privacy/camera_usage_description="" privacy/camera_usage_description_localized={} privacy/microphone_usage_description="" privacy/microphone_usage_description_localized={} privacy/photolibrary_usage_description="" privacy/photolibrary_usage_description_localized={} icons/iphone_120x120="" icons/iphone_180x180="" icons/ipad_76x76="" icons/ipad_152x152="" icons/ipad_167x167="" icons/app_store_1024x1024="res://appstore.png" icons/spotlight_40x40="" icons/spotlight_80x80="" icons/settings_58x58="" icons/settings_87x87="" icons/notification_40x40="" icons/notification_60x60="" storyboard/use_launch_screen_storyboard=false storyboard/image_scale_mode=0 storyboard/custom_image@2x="" storyboard/custom_image@3x="" storyboard/use_custom_bg_color=false storyboard/custom_bg_color=Color(0, 0, 0, 1) landscape_launch_screens/iphone_2436x1125="" landscape_launch_screens/iphone_2208x1242="" landscape_launch_screens/ipad_1024x768="" landscape_launch_screens/ipad_2048x1536="" portrait_launch_screens/iphone_640x960="" portrait_launch_screens/iphone_640x1136="" portrait_launch_screens/iphone_750x1334="" portrait_launch_screens/iphone_1125x2436="" portrait_launch_screens/ipad_768x1024="" portrait_launch_screens/ipad_1536x2048="" portrait_launch_screens/iphone_1242x2208="" application/name="" application/info="changeit" application/identifier="changeit" application/copyright="" capabilities/arkit=false capabilities/camera=false capabilities/game_center=true capabilities/in_app_purchases=false orientation/portrait=true orientation/landscape_left=true orientation/landscape_right=true orientation/portrait_upside_down=true required_icons/iphone_120x120="res://iOSIcons/Icon-App-60x60@2x.png" required_icons/ipad_76x76="res://iOSIcons/Icon-App-76x76@1x.png" required_icons/app_store_1024x1024="res://iOSIcons/ItunesArtwork@2x.png" optional_icons/iphone_180x180="res://iOSIcons/Icon-App-60x60@3x.png" optional_icons/ipad_152x152="res://iOSIcons/Icon-App-76x76@2x.png" optional_icons/ipad_167x167="res://iOSIcons/Icon-App-83.5x83.5@2x.png" optional_icons/spotlight_40x40="res://iOSIcons/Icon-App-40x40@1x.png" optional_icons/spotlight_80x80="res://iOSIcons/Icon-App-40x40@2x.png" architectures/armv7=false [preset.3] name="macOS" platform="macOS" runnable=true dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" export_path="../../../../Desktop/fmod-macos-export/FmodTestDemo.dmg" encryption_include_filters="" encryption_exclude_filters="" encrypt_pck=false encrypt_directory=false [preset.3.options] export/distribution_type=1 binary_format/architecture="universal" custom_template/debug="" custom_template/release="" debug/export_console_wrapper=1 application/icon="" application/icon_interpolation=4 application/bundle_identifier="com.utopiarise.godot.fmod" application/signature="" application/app_category="Games" application/short_version="1.0" application/version="1.0" application/copyright="" application/copyright_localized={} application/min_macos_version="10.12" display/high_res=true xcode/platform_build="14C18" xcode/sdk_version="13.1" xcode/sdk_build="22C55" xcode/sdk_name="macosx13.1" xcode/xcode_version="1420" xcode/xcode_build="14C18" codesign/codesign=3 codesign/installer_identity="" codesign/apple_team_id="" codesign/identity="" codesign/entitlements/custom_file="" codesign/entitlements/allow_jit_code_execution=false codesign/entitlements/allow_unsigned_executable_memory=false codesign/entitlements/allow_dyld_environment_variables=false codesign/entitlements/disable_library_validation=false codesign/entitlements/audio_input=false codesign/entitlements/camera=false codesign/entitlements/location=false codesign/entitlements/address_book=false codesign/entitlements/calendars=false codesign/entitlements/photos_library=false codesign/entitlements/apple_events=false codesign/entitlements/debugging=false codesign/entitlements/app_sandbox/enabled=false codesign/entitlements/app_sandbox/network_server=false codesign/entitlements/app_sandbox/network_client=false codesign/entitlements/app_sandbox/device_usb=false codesign/entitlements/app_sandbox/device_bluetooth=false codesign/entitlements/app_sandbox/files_downloads=0 codesign/entitlements/app_sandbox/files_pictures=0 codesign/entitlements/app_sandbox/files_music=0 codesign/entitlements/app_sandbox/files_movies=0 codesign/entitlements/app_sandbox/helper_executables=[] codesign/custom_options=PackedStringArray() notarization/notarization=0 privacy/microphone_usage_description="" privacy/microphone_usage_description_localized={} privacy/camera_usage_description="" privacy/camera_usage_description_localized={} privacy/location_usage_description="" privacy/location_usage_description_localized={} privacy/address_book_usage_description="" privacy/address_book_usage_description_localized={} privacy/calendar_usage_description="" privacy/calendar_usage_description_localized={} privacy/photos_library_usage_description="" privacy/photos_library_usage_description_localized={} privacy/desktop_folder_usage_description="" privacy/desktop_folder_usage_description_localized={} privacy/documents_folder_usage_description="" privacy/documents_folder_usage_description_localized={} privacy/downloads_folder_usage_description="" privacy/downloads_folder_usage_description_localized={} privacy/network_volumes_usage_description="" privacy/network_volumes_usage_description_localized={} privacy/removable_volumes_usage_description="" privacy/removable_volumes_usage_description_localized={} ssh_remote_deploy/enabled=false ssh_remote_deploy/host="user@host_ip" ssh_remote_deploy/port="22" ssh_remote_deploy/extra_args_ssh="" ssh_remote_deploy/extra_args_scp="" ssh_remote_deploy/run_script="#!/usr/bin/env bash unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}" ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\") rm -rf \"{temp_dir}\"" ================================================ FILE: demo/high_level_2D/ChangeColor.gd ================================================ extends Area2D var event: FmodEvent = null var icon: Sprite2D # Called when the node enters the scene tree for the first time. func _ready(): body_entered.connect(enter) body_exited.connect(leave) $FmodEventEmitter2D.paused = true # warning-ignore:unused_argument func enter(_area): print("enter") $FmodEventEmitter2D.paused = false # warning-ignore:unused_argument func leave(_area): print("leave") $FmodEventEmitter2D.paused = true # warning-ignore:unused_argument func change_color(_dict: Dictionary): $icon.self_modulate = Color(randf_range(0,1), randf_range(0,1), randf_range(0,1), 1) ================================================ FILE: demo/high_level_2D/ChangeColor.gd.uid ================================================ uid://ddkyglwgd83qf ================================================ FILE: demo/high_level_2D/ChooseLanguageButton.gd ================================================ class_name ChooseLanguageButton extends OptionButton @export var root_node: Node var current_bank_loader: FmodBankLoader = null func _enter_tree(): connect("item_selected", _on_item_selected) func _on_item_selected(index: int): if index == -1: return if (current_bank_loader != null): root_node.remove_child(current_bank_loader) var bank_path := "res://assets/Banks/Dialogue_%s.bank" % get_item_text(index) current_bank_loader = FmodBankLoader.new() current_bank_loader.bank_paths = [bank_path] root_node.add_child(current_bank_loader) ================================================ FILE: demo/high_level_2D/ChooseLanguageButton.gd.uid ================================================ uid://dlu46ug37ojmx ================================================ FILE: demo/high_level_2D/Emitter.gd ================================================ extends FmodEventEmitter2D var isPlaying: bool = true func _process(_delta): if Input.is_action_just_pressed("space"): isPlaying = !isPlaying if(isPlaying): print("Mower playing") paused = false else: print("Mower paused") paused = true elif Input.is_action_just_pressed("kill_event"): self.queue_free() if Input.is_action_pressed("engine_power_up"): self["fmod_parameters/RPM"] = self["fmod_parameters/RPM"] + 10 if Input.is_action_pressed("engine_power_down"): self["fmod_parameters/RPM"] = self["fmod_parameters/RPM"] - 10 ================================================ FILE: demo/high_level_2D/Emitter.gd.uid ================================================ uid://d2j35xjdrcpu0 ================================================ FILE: demo/high_level_2D/FmodNodesTest.tscn ================================================ [gd_scene load_steps=14 format=3 uid="uid://dl6g18ybwc83t"] [ext_resource type="Script" uid="uid://dbw46rru0a7ab" path="res://high_level_2D/sin_move.gd" id="1_2lkrj"] [ext_resource type="Script" uid="uid://d2j35xjdrcpu0" path="res://high_level_2D/Emitter.gd" id="2_5cntr"] [ext_resource type="Texture2D" uid="uid://qqnlquxquycf" path="res://icon.png" id="2_llv2n"] [ext_resource type="Script" uid="uid://bgrknjkxmlwqw" path="res://high_level_2D/Kinematic.gd" id="3_dlbku"] [ext_resource type="Script" uid="uid://ddkyglwgd83qf" path="res://high_level_2D/ChangeColor.gd" id="5_5p5kb"] [ext_resource type="Script" uid="uid://dae10jufdshyv" path="res://low_level_2D/EnterAndLeave.gd" id="5_jxvuy"] [ext_resource type="PackedScene" uid="uid://glfbseq2tmgg" path="res://high_level_2D/footstep.tscn" id="5_usogy"] [ext_resource type="Script" uid="uid://rxnfus6cxfle" path="res://low_level_2D/EnterandLeave2.gd" id="7_c28gt"] [ext_resource type="Script" uid="uid://dlu46ug37ojmx" path="res://high_level_2D/ChooseLanguageButton.gd" id="9_syqwk"] [ext_resource type="Script" uid="uid://bvjoomad02ag2" path="res://high_level_2D/SayWelcomeButton.gd" id="10_gwsas"] [sub_resource type="RectangleShape2D" id="1"] size = Vector2(87.7038, 82.621) [sub_resource type="RectangleShape2D" id="2"] resource_local_to_scene = true size = Vector2(288.124, 291.966) [sub_resource type="RectangleShape2D" id="3"] size = Vector2(289.938, 284.96) [node name="FmodNodesTest" type="Node"] [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", "res://assets/Banks/SFX.bank"] [node name="Node2D" type="Node2D" parent="FmodBankLoader"] position = Vector2(703, 486) script = ExtResource("1_2lkrj") [node name="Emitter" type="FmodEventEmitter2D" parent="FmodBankLoader/Node2D"] event_name = "event:/Vehicles/Car Engine" event_guid = "{0c8363b4-23af-4f9c-af4b-0951bfd37d84}" autoplay = true volume = 2.0 fmod_parameters/RPM/id = 5864137074015534804 fmod_parameters/RPM = 600.0 fmod_parameters/RPM/variant_type = 3 fmod_parameters/Load/id = -1795603775021193717 fmod_parameters/Load = -1.0 fmod_parameters/Load/variant_type = 3 self_modulate = Color(0.988235, 0, 0, 1) position = Vector2(-3.05176e-05, 0) script = ExtResource("2_5cntr") [node name="Label" type="Label" parent="FmodBankLoader/Node2D"] anchors_preset = 4 anchor_top = 0.5 anchor_bottom = 0.5 offset_left = 49.0 offset_top = -39.0 offset_right = 284.0 offset_bottom = 39.0 grow_vertical = 2 size_flags_stretch_ratio = 0.0 text = "Press Space to pause/unpause Come closer to hear it Press Up arrow and down arrow to modify engine rpm" [node name="Sprite" type="Sprite2D" parent="FmodBankLoader/Node2D"] self_modulate = Color(0.988235, 0, 0, 1) position = Vector2(-3.05176e-05, 0) texture = ExtResource("2_llv2n") [node name="Listener" type="CharacterBody2D" parent="FmodBankLoader"] position = Vector2(500, 150) script = ExtResource("3_dlbku") footstep_scene = ExtResource("5_usogy") [node name="FmodListener2D" type="FmodListener2D" parent="FmodBankLoader/Listener"] [node name="icon" type="Sprite2D" parent="FmodBankLoader/Listener"] position = Vector2(1.89996, 4.12415) texture = ExtResource("2_llv2n") [node name="CollisionShape2D" type="CollisionShape2D" parent="FmodBankLoader/Listener"] position = Vector2(0.440125, 0.440125) shape = SubResource("1") [node name="Label" type="Label" parent="FmodBankLoader/Listener"] anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 grow_horizontal = 2 size_flags_stretch_ratio = 0.0 text = "Listener Kill it with K key!" [node name="SoundArea1" type="Area2D" parent="FmodBankLoader"] position = Vector2(146.558, 148.68) script = ExtResource("5_jxvuy") [node name="CollisionShape2D" type="CollisionShape2D" parent="FmodBankLoader/SoundArea1"] shape = SubResource("2") [node name="icon" type="Sprite2D" parent="FmodBankLoader/SoundArea1"] self_modulate = Color(0.113725, 0.823529, 0.317647, 1) z_index = -1 scale = Vector2(3, 3) texture = ExtResource("2_llv2n") [node name="Label2" type="Label" parent="FmodBankLoader/SoundArea1"] size_flags_stretch_ratio = 0.0 text = "Files loaded as sounds. Played when entering and exiting this area. Several instances of the same sound can be played at the same time " [node name="SoundArea2" type="Area2D" parent="FmodBankLoader"] position = Vector2(818.43, 97.5364) script = ExtResource("5_5p5kb") [node name="FmodEventEmitter2D" type="FmodEventEmitter2D" parent="FmodBankLoader/SoundArea2"] event_name = "event:/Music/Level 02" event_guid = "{c7f946fd-d695-499b-a820-752799c4921d}" autoplay = true [node name="CollisionShape2D" type="CollisionShape2D" parent="FmodBankLoader/SoundArea2"] position = Vector2(52.8021, 45.8286) shape = SubResource("3") [node name="icon" type="Sprite2D" parent="FmodBankLoader/SoundArea2"] self_modulate = Color(0.0117647, 0.956863, 0.0156863, 1) z_index = -1 position = Vector2(52.3218, 46.0544) scale = Vector2(3, 3) texture = ExtResource("2_llv2n") [node name="Label3" type="Label" parent="FmodBankLoader/SoundArea2"] size_flags_stretch_ratio = 0.0 text = "Event is unpaused when entering The color changes every beat" [node name="SoundArea3" type="Area2D" parent="FmodBankLoader"] position = Vector2(91.9974, 414.5) script = ExtResource("7_c28gt") [node name="CollisionShape2D" type="CollisionShape2D" parent="FmodBankLoader/SoundArea3"] position = Vector2(52.8021, 45.8286) shape = SubResource("3") [node name="icon" type="Sprite2D" parent="FmodBankLoader/SoundArea3"] self_modulate = Color(0.827451, 0.345098, 0.0941176, 1) z_index = -1 position = Vector2(52.3218, 46.0544) scale = Vector2(3, 3) texture = ExtResource("2_llv2n") [node name="Label3" type="Label" parent="FmodBankLoader/SoundArea3"] size_flags_stretch_ratio = 0.0 text = "File loaded as Music Start when entering Stop when exiting Only one instance of that music can be played " [node name="ChooseLanguageButton" type="OptionButton" parent="." node_paths=PackedStringArray("root_node")] offset_left = 918.0 offset_top = 615.0 offset_right = 986.0 offset_bottom = 646.0 item_count = 3 popup/item_0/text = "CN" popup/item_0/id = 0 popup/item_1/text = "EN" popup/item_1/id = 1 popup/item_2/text = "JP" popup/item_2/id = 2 script = ExtResource("9_syqwk") root_node = NodePath("..") [node name="SayWelcomeButton" type="Button" parent="." node_paths=PackedStringArray("welcome_option_button")] offset_left = 987.0 offset_top = 615.0 offset_right = 1100.0 offset_bottom = 646.0 text = "Say welcome" script = ExtResource("10_gwsas") welcome_option_button = NodePath("../ChooseLanguageButton") ================================================ FILE: demo/high_level_2D/Kinematic.gd ================================================ extends CharacterBody2D var distance_traveled := 0 @export var footstep_scene: PackedScene @onready var foot_step_emitter: FmodEventEmitter2D = footstep_scene.instantiate() func _ready() -> void: add_child(foot_step_emitter) func _process(delta): var direction: Vector2 = Vector2(0,0) var rotation_dir = 0 if Input.is_action_pressed("right"): direction.x += 1 if Input.is_action_pressed("left"): direction.x -= 1 if Input.is_action_pressed("up"): direction.y -= 1 if Input.is_action_pressed("down"): direction.y += 1 if Input.is_action_pressed("rotate_right"): rotation_dir = 1 if Input.is_action_pressed("rotate_left"): rotation_dir = -1 if direction != Vector2(0,0): distance_traveled += delta * 200 if distance_traveled >= 35: distance_traveled = 0 foot_step_emitter.play_one_shot() direction = direction.normalized() direction.x = direction.x * delta * 200 direction.y = direction.y * delta * 200 self.position += direction self.rotate(rotation_dir * delta * 5) if Input.is_action_pressed("lock_listener"): $FmodListener2D.is_locked = !$FmodListener2D.is_locked elif Input.is_action_pressed("kill"): self.queue_free() ================================================ FILE: demo/high_level_2D/Kinematic.gd.uid ================================================ uid://bgrknjkxmlwqw ================================================ FILE: demo/high_level_2D/SayWelcomeButton.gd ================================================ extends Button @export var welcome_option_button: ChooseLanguageButton func _enter_tree(): connect("pressed", _on_pressed) func _on_pressed(): if welcome_option_button.current_bank_loader == null: return var event_emitter = FmodEventEmitter2D.new() event_emitter.event_guid = "{9aa2ecc5-ea4b-4ebe-85c3-054b11b21dcd}" event_emitter.attached = false event_emitter.autoplay = true event_emitter.auto_release = true event_emitter.set_programmer_callback("welcome") welcome_option_button.current_bank_loader.add_child(event_emitter) ================================================ FILE: demo/high_level_2D/SayWelcomeButton.gd.uid ================================================ uid://bvjoomad02ag2 ================================================ FILE: demo/high_level_2D/footstep.tscn ================================================ [gd_scene format=3 uid="uid://glfbseq2tmgg"] [node name="Footstep" type="FmodEventEmitter2D"] event_guid = "{e905d401-76f5-4741-a885-f7844b244671}" attached = false allow_fadeout = false ================================================ FILE: demo/high_level_2D/sin_move.gd ================================================ extends Node2D func _process(delta: float) -> void: var time = Time.get_ticks_msec()/1000.0 self.position.x = 700 + 300 * sin(time) ================================================ FILE: demo/high_level_2D/sin_move.gd.uid ================================================ uid://dbw46rru0a7ab ================================================ FILE: demo/high_level_3D/FPSCounter.gd ================================================ extends Label func _ready(): set_process(true) func _process(_delta: float): self.text = 'FPS: %s' % Engine.get_frames_per_second() ================================================ FILE: demo/high_level_3D/FPSCounter.gd.uid ================================================ uid://dqy6qt50sgnnw ================================================ FILE: demo/high_level_3D/World.tscn ================================================ [gd_scene load_steps=13 format=3 uid="uid://dk02rm1jcir6t"] [ext_resource type="Script" uid="uid://dqy6qt50sgnnw" path="res://high_level_3D/FPSCounter.gd" id="1_sjwuc"] [ext_resource type="PackedScene" uid="uid://bhw2o0powjnsp" path="res://high_level_3D/environment/Floor.tscn" id="2_kesb6"] [ext_resource type="PackedScene" uid="uid://dl8xj04oxmnsb" path="res://high_level_3D/environment/Ball.tscn" id="3_bkia1"] [ext_resource type="Script" uid="uid://vfnvt7s745x1" path="res://high_level_3D/environment/sin_move.gd" id="4_ewod2"] [ext_resource type="PackedScene" uid="uid://c7isdpd8ykjep" path="res://high_level_3D/environment/Wall.tscn" id="4_jv1x4"] [ext_resource type="Script" uid="uid://c2i08gks60jr0" path="res://high_level_3D/environment/soundcollider.gd" id="4_mxf3j"] [ext_resource type="Script" uid="uid://drfwohij4miwv" path="res://high_level_3D/selfdestroy.gd" id="4_vlj6k"] [ext_resource type="Script" uid="uid://dnmapedhp0cbt" path="res://high_level_3D/rollingball.gd" id="5_d681a"] [ext_resource type="PackedScene" uid="uid://bsguup0m8xqxp" path="res://high_level_3D/player/Player.tscn" id="5_i7hmm"] [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_y252g"] [sub_resource type="Sky" id="8"] sky_material = SubResource("ProceduralSkyMaterial_y252g") [sub_resource type="Environment" id="7"] background_mode = 2 sky = SubResource("8") ssao_intensity = 4.0 [node name="Node3D" type="Node3D"] [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", "res://assets/Banks/SFX.bank"] [node name="FPSCounter" type="Label" parent="."] offset_right = 40.0 offset_bottom = 14.0 text = "FPS: 0" script = ExtResource("1_sjwuc") [node name="Help" type="Label" parent="."] offset_top = 20.0 offset_right = 106.0 offset_bottom = 38.0 text = "Space = Jump WASD = Move" [node name="Floor" parent="." instance=ExtResource("2_kesb6")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.007, -2, -3.715) [node name="Ball1" parent="." instance=ExtResource("3_bkia1")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 3.2038) freeze = true script = ExtResource("4_ewod2") [node name="FmodEventEmitter3D" type="FmodEventEmitter3D" parent="Ball1"] event_name = "event:/Vehicles/Car Engine" event_guid = "{0c8363b4-23af-4f9c-af4b-0951bfd37d84}" autoplay = true fmod_parameters/RPM/id = 5864137074015534804 fmod_parameters/RPM = 600.0 fmod_parameters/RPM/variant_type = 3 fmod_parameters/Load/id = -1795603775021193717 fmod_parameters/Load = -1.0 fmod_parameters/Load/variant_type = 3 script = ExtResource("4_vlj6k") [node name="Ball2" parent="." instance=ExtResource("3_bkia1")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, -1.07225, -3) sleeping = true [node name="FmodEventEmitter3D" type="FmodEventEmitter3D" parent="Ball2"] event_name = "event:/Weapons/Explosion" event_guid = "{1f687138-e06c-40f5-9bac-57f84bbcedd3}" volume = 0.5 fmod_parameters/Size/id = 6419405856426461066 fmod_parameters/Size = 0.5 fmod_parameters/Size/variant_type = 3 fmod_parameters/Distance/id = -6363846794978107960 fmod_parameters/Distance = 3.40282e+38 fmod_parameters/Distance/variant_type = 3 script = ExtResource("4_mxf3j") [node name="Ball3" parent="." instance=ExtResource("3_bkia1")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, -1.03226, -3) [node name="FmodEventEmitter3D" type="FmodEventEmitter3D" parent="Ball3"] event_name = "event:/Interactables/Barrel Roll" event_guid = "{c42c2240-c4b6-42ed-a473-1a47f19945ea}" autoplay = true volume = 0.5 fmod_parameters/Speed/id = 841507833874797062 fmod_parameters/Speed = 0.0 fmod_parameters/Speed/variant_type = 3 script = ExtResource("5_d681a") [node name="Wall" parent="." instance=ExtResource("4_jv1x4")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.665272, -8.1876) [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("7") [node name="Sun" type="DirectionalLight3D" parent="WorldEnvironment"] transform = Transform3D(-0.5, -0.296198, 0.813798, 0, 0.939693, 0.34202, -0.866025, 0.17101, -0.469847, 0, 0, 0) layers = 262144 light_color = Color(0.94902, 0.580392, 0.247059, 1) light_cull_mask = 4294443007 shadow_enabled = true [node name="Player" parent="." instance=ExtResource("5_i7hmm")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1.47008, 8.47787) [node name="FmodListener3D" type="FmodListener3D" parent="Player"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.56489, 0) ================================================ FILE: demo/high_level_3D/environment/1x1.png.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://dd1hsqwhgfly3" path.s3tc="res://.godot/imported/1x1.png-4d9e3047873d92d90ea5851d12e7ef58.s3tc.ctex" path.etc2="res://.godot/imported/1x1.png-4d9e3047873d92d90ea5851d12e7ef58.etc2.ctex" metadata={ "imported_formats": ["s3tc_bptc", "etc2_astc"], "vram_texture": true } [deps] source_file="res://high_level_3D/environment/1x1.png" dest_files=["res://.godot/imported/1x1.png-4d9e3047873d92d90ea5851d12e7ef58.s3tc.ctex", "res://.godot/imported/1x1.png-4d9e3047873d92d90ea5851d12e7ef58.etc2.ctex"] [params] compress/mode=2 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=true 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=0 ================================================ FILE: demo/high_level_3D/environment/Ball.tscn ================================================ [gd_scene load_steps=4 format=3 uid="uid://dl8xj04oxmnsb"] [ext_resource type="Material" path="res://high_level_3D/environment/ball_material.tres" id="1_4hjdv"] [sub_resource type="SphereShape3D" id="7"] [sub_resource type="SphereMesh" id="8"] material = ExtResource("1_4hjdv") [node name="Ball" type="RigidBody3D"] linear_damp_mode = 1 linear_damp = 2.0 angular_damp_mode = 1 angular_damp = 3.0 [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("7") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] mesh = SubResource("8") ================================================ FILE: demo/high_level_3D/environment/Floor.tscn ================================================ [gd_scene load_steps=4 format=3 uid="uid://bhw2o0powjnsp"] [ext_resource type="Material" path="res://high_level_3D/environment/wall_material.tres" id="1_2s467"] [sub_resource type="BoxMesh" id="4"] material = ExtResource("1_2s467") [sub_resource type="BoxShape3D" id="3"] size = Vector3(64, 1, 64) [node name="Floor" type="StaticBody3D"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2.0364, 0) [node name="Mesh" type="MeshInstance3D" parent="."] transform = Transform3D(64, 0, 0, 0, 1, 0, 0, 0, 64, 0, 0, 0) mesh = SubResource("4") skeleton = NodePath("../..") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("3") ================================================ FILE: demo/high_level_3D/environment/Wall.tscn ================================================ [gd_scene load_steps=8 format=3 uid="uid://c7isdpd8ykjep"] [ext_resource type="Material" path="res://high_level_3D/environment/wall_material.tres" id="2"] [sub_resource type="BoxMesh" id="9"] material = ExtResource("2") size = Vector3(20, 5, 5) [sub_resource type="PrismMesh" id="12"] material = ExtResource("2") left_to_right = 0.0 size = Vector3(10, 5, 5) [sub_resource type="ConcavePolygonShape3D" id="15"] data = PackedVector3Array(-5, 2.5, 2.5, 5, -2.5, 2.5, -5, -2.5, 2.5, -5, 2.5, -2.5, -5, -2.5, -2.5, 5, -2.5, -2.5, -5, 2.5, 2.5, -5, 2.5, -2.5, 5, -2.5, 2.5, -5, 2.5, -2.5, 5, -2.5, -2.5, 5, -2.5, 2.5, -5, 2.5, -2.5, -5, 2.5, 2.5, -5, -2.5, -2.5, -5, 2.5, 2.5, -5, -2.5, 2.5, -5, -2.5, -2.5, -5, -2.5, 2.5, 5, -2.5, 2.5, -5, -2.5, -2.5, 5, -2.5, 2.5, 5, -2.5, -2.5, -5, -2.5, -2.5) [sub_resource type="PrismMesh" id="14"] material = ExtResource("2") left_to_right = 1.0 size = Vector3(10, 5, 5) [sub_resource type="ConcavePolygonShape3D" id="16"] data = PackedVector3Array(5, 2.5, 2.5, 5, -2.5, 2.5, -5, -2.5, 2.5, 5, 2.5, -2.5, -5, -2.5, -2.5, 5, -2.5, -2.5, 5, 2.5, 2.5, 5, 2.5, -2.5, 5, -2.5, 2.5, 5, 2.5, -2.5, 5, -2.5, -2.5, 5, -2.5, 2.5, 5, 2.5, -2.5, 5, 2.5, 2.5, -5, -2.5, -2.5, 5, 2.5, 2.5, -5, -2.5, 2.5, -5, -2.5, -2.5, -5, -2.5, 2.5, 5, -2.5, 2.5, -5, -2.5, -2.5, 5, -2.5, 2.5, 5, -2.5, -2.5, -5, -2.5, -2.5) [sub_resource type="ConcavePolygonShape3D" id="17"] data = PackedVector3Array(-10, 2.5, 2.5, 10, 2.5, 2.5, -10, -2.5, 2.5, 10, 2.5, 2.5, 10, -2.5, 2.5, -10, -2.5, 2.5, 10, 2.5, -2.5, -10, 2.5, -2.5, 10, -2.5, -2.5, -10, 2.5, -2.5, -10, -2.5, -2.5, 10, -2.5, -2.5, 10, 2.5, 2.5, 10, 2.5, -2.5, 10, -2.5, 2.5, 10, 2.5, -2.5, 10, -2.5, -2.5, 10, -2.5, 2.5, -10, 2.5, -2.5, -10, 2.5, 2.5, -10, -2.5, -2.5, -10, 2.5, 2.5, -10, -2.5, 2.5, -10, -2.5, -2.5, 10, 2.5, 2.5, -10, 2.5, 2.5, 10, 2.5, -2.5, -10, 2.5, 2.5, -10, 2.5, -2.5, 10, 2.5, -2.5, -10, -2.5, 2.5, 10, -2.5, 2.5, -10, -2.5, -2.5, 10, -2.5, 2.5, 10, -2.5, -2.5, -10, -2.5, -2.5) [node name="Wall" type="MeshInstance3D"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35862, -5.6476) mesh = SubResource("9") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 0, 0) mesh = SubResource("12") [node name="StaticBody3D" type="StaticBody3D" parent="MeshInstance3D"] [node name="CollisionShape3D" type="CollisionShape3D" parent="MeshInstance3D/StaticBody3D"] shape = SubResource("15") [node name="MeshInstance2" type="MeshInstance3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 0) mesh = SubResource("14") [node name="StaticBody3D" type="StaticBody3D" parent="MeshInstance2"] [node name="CollisionShape3D" type="CollisionShape3D" parent="MeshInstance2/StaticBody3D"] shape = SubResource("16") [node name="StaticBody3D" type="StaticBody3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"] shape = SubResource("17") ================================================ FILE: demo/high_level_3D/environment/ball_material.tres ================================================ [gd_resource type="StandardMaterial3D" load_steps=2 format=3] [ext_resource type="Texture2D" uid="uid://b8p3qvgwqt8dd" path="res://high_level_3D/environment/1x1.png" id="1"] [resource] detail_enabled = true detail_blend_mode = 0 detail_uv_layer = 0 detail_albedo = ExtResource( 1 ) uv1_scale = Vector3( 0.5, 0.5, 0.5 ) ================================================ FILE: demo/high_level_3D/environment/box.tscn ================================================ [gd_scene load_steps=4 format=3 uid="uid://cnhrhyfu1hs1t"] [ext_resource type="Material" path="res://high_level_3D/environment/wall_material.tres" id="1_uit7m"] [sub_resource type="BoxMesh" id="BoxMesh_wnaer"] material = ExtResource("1_uit7m") size = Vector3(0.3, 2, 2) [sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_1j8j0"] points = PackedVector3Array(0.15, 1, 1, -0.15, 1, 1, 0.15, -1, 1, 0.15, 1, -1, -0.15, 1, -1, -0.15, -1, 1, 0.15, -1, -1, -0.15, -1, -1) [node name="Box" type="RigidBody3D"] mass = 4.0 [node name="MeshInstance3D" type="MeshInstance3D" parent="."] mesh = SubResource("BoxMesh_wnaer") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("ConvexPolygonShape3D_1j8j0") ================================================ FILE: demo/high_level_3D/environment/sin_move.gd ================================================ extends RigidBody3D func _process(delta: float) -> void: var time = Time.get_ticks_msec()/1000.0 self.position.x = 10 * sin(time) ================================================ FILE: demo/high_level_3D/environment/sin_move.gd.uid ================================================ uid://vfnvt7s745x1 ================================================ FILE: demo/high_level_3D/environment/soundcollider.gd ================================================ extends FmodEventEmitter3D var moving := false func _process(_delta: float): var parent: RigidBody3D = get_parent() if moving == false and parent.linear_velocity.length() > 1: moving = true self.play() elif parent.linear_velocity.length() < 1: moving = false ================================================ FILE: demo/high_level_3D/environment/soundcollider.gd.uid ================================================ uid://c2i08gks60jr0 ================================================ FILE: demo/high_level_3D/environment/wall_material.tres ================================================ [gd_resource type="StandardMaterial3D" load_steps=2 format=3] [ext_resource type="Texture2D" uid="uid://b8p3qvgwqt8dd" path="res://high_level_3D/environment/1x1.png" id="1"] [resource] detail_enabled = true detail_blend_mode = 0 detail_uv_layer = 0 detail_albedo = ExtResource( 1 ) uv1_scale = Vector3( 20, 15, 0 ) ================================================ FILE: demo/high_level_3D/player/Camera.gd ================================================ extends Camera3D @onready var Player = get_parent() ## Increase this value to give a slower turn speed const CAMERA_TURN_SPEED = 200 func _ready(): ## Tell Godot that we want to handle input set_process_input(true) func look_updown_rotation(new_rotation = 0): """ Returns a new Vector3 which contains only the x direction We'll use this vector to compute the final 3D rotation later """ var toReturn = self.get_rotation() + Vector3(new_rotation, 0, 0) ## ## We don't want the player to be able to bend over backwards ## neither to be able to look under their arse. ## Here we'll clamp the vertical look to 90° up and down toReturn.x = clamp(toReturn.x, PI / -2, PI / 2) return toReturn func look_leftright_rotation(new_rotation = 0): """ Returns a new Vector3 which contains only the y direction We'll use this vector to compute the final 3D rotation later """ return Player.get_rotation() + Vector3(0, new_rotation, 0) func _input(event): """ First person camera controls """ ## ## We'll only process mouse motion events if not event is InputEventMouseMotion: return ## ## We'll use the parent node "Player" to set our left-right rotation ## This prevents us from adding the x-rotation to the y-rotation ## which would result in a kind of flight-simulator camera Player.set_rotation(look_leftright_rotation(event.relative.x / -CAMERA_TURN_SPEED)) ## ## Now we can simply set our y-rotation for the camera, and let godot ## handle the transformation of both together self.set_rotation(look_updown_rotation(event.relative.y / -CAMERA_TURN_SPEED)) func _enter_tree(): """ Hide the mouse when we start """ Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _leave_tree(): """ Show the mouse when we leave """ Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) ================================================ FILE: demo/high_level_3D/player/Camera.gd.uid ================================================ uid://brjp8swvid6no ================================================ FILE: demo/high_level_3D/player/Player.gd ================================================ extends CharacterBody3D var direction := Vector3(0, 0, 0) # Used for animation var will_jump := false const JUMP := 4 const PLAYER_MOVE_SPEED := 4 @onready var Camera = $Camera3D @onready var GRAVITY = ProjectSettings.get("physics/3d/default_gravity") / 1000 func do_jump() -> void: if not self.is_on_floor(): return if self.will_jump: return will_jump = true await get_tree().create_timer(0.05).timeout self.velocity.y += JUMP await get_tree().create_timer(0.1).timeout will_jump = false func _process(_delta: float) -> void: """ Allow the player to move the camera with WASD See Project settings -> Input map for keyboard bindings """ if Input.is_action_just_pressed("space"): self.do_jump() var amount: float = 1 if not is_on_floor() or will_jump: amount = 0.2 if Input.is_action_pressed("up"): self.direction.z -= amount elif Input.is_action_pressed("down"): self.direction.z += amount if Input.is_action_pressed("left"): self.direction.x -= amount elif Input.is_action_pressed("right"): self.direction.x += amount self.direction = self.direction.clamp(Vector3(-1, -1, -1), Vector3(1, 1, 1)) func _physics_process(delta: float) -> void: # Apply friction if self.is_on_floor(): self.direction *= Vector3.ONE - Vector3(0.9, 1.0, 0.9) * (10 * delta) # Preserve the Y velocity from the previous frame self.velocity = Vector3(0, self.velocity.y, 0) # Always add velocity even when we're in the air self.velocity += get_transform().basis.x * direction.x * PLAYER_MOVE_SPEED self.velocity += get_transform().basis.z * direction.z * PLAYER_MOVE_SPEED # Apply less gravity if we were on the floor last frame # This helps our KinematicBody to avoid physics jitter if self.is_on_floor(): self.velocity -= Vector3(0, GRAVITY / 100, 0) else: self.velocity -= Vector3(0, GRAVITY, 0) self.move_and_slide() for i in get_slide_collision_count(): var collision = get_slide_collision(i) var collider = collision.get_collider() if not collider is RigidBody3D: continue collider.apply_central_impulse(-collision.get_normal() * 0.8) collider.apply_impulse(-collision.get_normal() * 0.01, collision.get_position()) ================================================ FILE: demo/high_level_3D/player/Player.gd.uid ================================================ uid://b0wpksi2hoyfn ================================================ FILE: demo/high_level_3D/player/Player.tscn ================================================ [gd_scene load_steps=4 format=3 uid="uid://bsguup0m8xqxp"] [ext_resource type="Script" uid="uid://b0wpksi2hoyfn" path="res://high_level_3D/player/Player.gd" id="1"] [ext_resource type="Script" uid="uid://brjp8swvid6no" path="res://high_level_3D/player/Camera.gd" id="2_fstpc"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_di3pi"] [node name="Player" type="CharacterBody3D"] platform_on_leave = 2 script = ExtResource("1") [node name="Camera3D" type="Camera3D" parent="."] transform = Transform3D(1, 9.09495e-15, 0, -9.09495e-15, 1, 0, 0, 0, 1, -9.31323e-10, 1.67742, 0.13534) cull_mask = 524287 current = true fov = 50.0 far = 200.0 script = ExtResource("2_fstpc") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0151734, 1.01978, 0.234704) shape = SubResource("CapsuleShape3D_di3pi") ================================================ FILE: demo/high_level_3D/rollingball.gd ================================================ extends FmodEventEmitter3D func _process(_delta: float): var parent: RigidBody3D = get_parent() var value:= parent.angular_velocity.length() / 2 if value < 0.1: value = 0 self["fmod_parameters/Speed"] = value ================================================ FILE: demo/high_level_3D/rollingball.gd.uid ================================================ uid://dnmapedhp0cbt ================================================ FILE: demo/high_level_3D/selfdestroy.gd ================================================ extends FmodEventEmitter3D func _process(delta: float) -> void: if Input.is_action_just_pressed("kill"): self.queue_free() ================================================ FILE: demo/high_level_3D/selfdestroy.gd.uid ================================================ uid://drfwohij4miwv ================================================ FILE: demo/icon.png.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://qqnlquxquycf" path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false } [deps] source_file="res://icon.png" dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.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/icon.svg.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://cih2yjmtjoohg" path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" metadata={ "vram_texture": false } [deps] source_file="res://icon.svg" dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] [params] compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 compress/uastc_level=0 compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" process/channel_remap/red=0 process/channel_remap/green=1 process/channel_remap/blue=2 process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 detect_3d/compress_to=1 svg/scale=1.0 editor/scale_with_editor_scale=false editor/convert_colors_with_editor_theme=false ================================================ FILE: demo/low_level_2D/ChangeColor.gd ================================================ extends Area2D var event: FmodEvent = null var icon: Sprite2D # Called when the node enters the scene tree for the first time. func _ready(): event = FmodServer.create_event_instance("event:/Music/Level 02") event.set_callback(Callable(self, "change_color"), FmodServer.FMOD_STUDIO_EVENT_CALLBACK_ALL) body_entered.connect(enter) body_exited.connect(leave) event.start() event.set_paused(true) # warning-ignore:unused_argument func enter(_area): print("enter") event.set_paused(false) # warning-ignore:unused_argument func leave(_area): print("leave") event.set_paused(true) # warning-ignore:unused_argument func change_color(_dict: Dictionary, type: int): if type == FmodServer.FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT: $icon.self_modulate = Color(randf_range(0,1), randf_range(0,1), randf_range(0,1), 1) ================================================ FILE: demo/low_level_2D/ChangeColor.gd.uid ================================================ uid://be1jwed20nkjr ================================================ FILE: demo/low_level_2D/Emitter.gd ================================================ extends Sprite2D var isPlaying: bool = true var event: FmodEvent = null # Called when the node enters the scene tree for the first time. func _ready(): event = FmodServer.create_event_instance("event:/Vehicles/Car Engine") event.set_2d_attributes(self.global_transform) event.set_parameter_by_name("RPM", 600) event.set_volume( 2) event.start() # warning-ignore:unused_argument func _process(_delta): if Input.is_action_just_pressed("space"): isPlaying = !isPlaying if(isPlaying): print("Mower playing") event.set_paused(false) else: print("Mower paused") event.set_paused(true) elif Input.is_action_just_pressed("kill_event"): self.queue_free() var time = Time.get_ticks_msec()/1000.0 self.position.x = 300 * sin(time) event.set_2d_attributes(self.global_transform) ================================================ FILE: demo/low_level_2D/Emitter.gd.uid ================================================ uid://cxg2l03odbmwv ================================================ FILE: demo/low_level_2D/EnterAndLeave.gd ================================================ extends Area2D # Declare member variables here. Examples: # var a = 2 var open_sound: FmodSound = null var close_sound: FmodSound = null # Called when the node enters the scene tree for the first time. func _ready(): FmodServer.load_file_as_sound("res://assets/Sounds/doorOpen_1.ogg") FmodServer.load_file_as_sound("res://assets/Sounds/doorClose_1.ogg") # warning-ignore:return_value_discarded body_entered.connect(enter) # warning-ignore:return_value_discarded body_exited.connect(leave) # warning-ignore:unused_argument func enter(_area): print("enter") open_sound = FmodServer.create_sound_instance("res://assets/Sounds/doorOpen_1.ogg") open_sound.set_pitch(randf_range(0.75,1.25)) open_sound.play() # warning-ignore:unused_argument func leave(_area): print("leave") close_sound = FmodServer.create_sound_instance("res://assets/Sounds/doorClose_1.ogg") close_sound.set_pitch(randf_range(0.75,1.5)) close_sound.play() func _exit_tree(): FmodServer.unload_file("res://assets/Sounds/doorOpen_1.ogg") FmodServer.unload_file("res://assets/Sounds/doorClose_1.ogg") ================================================ FILE: demo/low_level_2D/EnterAndLeave.gd.uid ================================================ uid://dae10jufdshyv ================================================ FILE: demo/low_level_2D/EnterandLeave2.gd ================================================ extends Area2D # Declare member variables here. Examples: # var a = 2 var music: FmodSound = null # Called when the node enters the scene tree for the first time. func _ready(): FmodServer.load_file_as_music("res://assets/Music/jingles_SAX07.ogg") # warning-ignore:return_value_discarded body_entered.connect(enter) # warning-ignore:return_value_discarded body_exited.connect(leave) # warning-ignore:unused_argument func enter(_area): print("enter") music = FmodServer.create_sound_instance("res://assets/Music/jingles_SAX07.ogg") music.play() # warning-ignore:unused_argument func leave(_area): print("leave") music.release() func _exit_tree(): FmodServer.unload_file("res://assets/Music/jingles_SAX07.ogg") ================================================ FILE: demo/low_level_2D/EnterandLeave2.gd.uid ================================================ uid://rxnfus6cxfle ================================================ FILE: demo/low_level_2D/FmodScriptTest.tscn ================================================ [gd_scene load_steps=13 format=3 uid="uid://cs8nm6h12whh1"] [ext_resource type="Script" uid="uid://dn0b4rle1712" path="res://low_level_2D/FmodTest.gd" id="1_oc8v3"] [ext_resource type="Texture2D" uid="uid://qqnlquxquycf" path="res://icon.png" id="2_mamb7"] [ext_resource type="Script" uid="uid://cxg2l03odbmwv" path="res://low_level_2D/Emitter.gd" id="3_fx7d3"] [ext_resource type="Script" uid="uid://c4p1w0xlxjs4l" path="res://low_level_2D/Listener.gd" id="4_448uv"] [ext_resource type="Script" uid="uid://dae10jufdshyv" path="res://low_level_2D/EnterAndLeave.gd" id="5_85yno"] [ext_resource type="Script" uid="uid://be1jwed20nkjr" path="res://low_level_2D/ChangeColor.gd" id="6_fyoq1"] [ext_resource type="Script" uid="uid://rxnfus6cxfle" path="res://low_level_2D/EnterandLeave2.gd" id="7_6vajh"] [ext_resource type="Script" uid="uid://bd3konietc7md" path="res://low_level_2D/LangChooseButton.gd" id="8_f6f5a"] [ext_resource type="Script" uid="uid://cpd21piypux6c" path="res://low_level_2D/WelcomeButton.gd" id="9_jo4tj"] [sub_resource type="RectangleShape2D" id="1"] size = Vector2(87.7038, 82.621) [sub_resource type="RectangleShape2D" id="2"] resource_local_to_scene = true size = Vector2(288.124, 291.966) [sub_resource type="RectangleShape2D" id="3"] size = Vector2(289.938, 284.96) [node name="FmodTest" type="Node2D"] script = ExtResource("1_oc8v3") [node name="Node2D" type="Node2D" parent="."] position = Vector2(691, 495) [node name="Emitter" type="Sprite2D" parent="Node2D"] self_modulate = Color(0.988235, 0, 0, 1) position = Vector2(-3.05176e-05, 0) texture = ExtResource("2_mamb7") script = ExtResource("3_fx7d3") [node name="Label" type="Label" parent="Node2D"] anchors_preset = 4 anchor_top = 0.5 anchor_bottom = 0.5 grow_vertical = 2 size_flags_stretch_ratio = 0.0 text = "Press Space to pause/unpause Come closer to hear it" [node name="Listener" type="CharacterBody2D" parent="."] position = Vector2(500, 150) script = ExtResource("4_448uv") [node name="icon" type="Sprite2D" parent="Listener"] position = Vector2(1.89996, 4.12415) texture = ExtResource("2_mamb7") [node name="CollisionShape2D" type="CollisionShape2D" parent="Listener"] position = Vector2(0.440125, 0.440125) shape = SubResource("1") [node name="Label" type="Label" parent="Listener"] anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 grow_horizontal = 2 size_flags_stretch_ratio = 0.0 text = "Listener Kill it with K key!" [node name="SoundArea1" type="Area2D" parent="."] position = Vector2(146.558, 148.68) script = ExtResource("5_85yno") [node name="CollisionShape2D" type="CollisionShape2D" parent="SoundArea1"] shape = SubResource("2") [node name="icon" type="Sprite2D" parent="SoundArea1"] self_modulate = Color(0.113725, 0.823529, 0.317647, 1) z_index = -1 scale = Vector2(3, 3) texture = ExtResource("2_mamb7") [node name="Label2" type="Label" parent="SoundArea1"] size_flags_stretch_ratio = 0.0 text = "Files loaded as sounds. Played when entering and exiting this area. Several instances of the same sound can be played at the same time " [node name="SoundArea2" type="Area2D" parent="."] position = Vector2(948, 98) script = ExtResource("6_fyoq1") [node name="CollisionShape2D" type="CollisionShape2D" parent="SoundArea2"] position = Vector2(52.8021, 45.8286) shape = SubResource("3") [node name="icon" type="Sprite2D" parent="SoundArea2"] self_modulate = Color(0.0117647, 0.956863, 0.0156863, 1) z_index = -1 position = Vector2(52.3218, 46.0544) scale = Vector2(3, 3) texture = ExtResource("2_mamb7") [node name="Label3" type="Label" parent="SoundArea2"] size_flags_stretch_ratio = 0.0 text = "Event is unpaused when entering The color changes every beat" [node name="SoundArea3" type="Area2D" parent="."] position = Vector2(91.9974, 414.5) script = ExtResource("7_6vajh") [node name="CollisionShape2D" type="CollisionShape2D" parent="SoundArea3"] position = Vector2(52.8021, 45.8286) shape = SubResource("3") [node name="icon" type="Sprite2D" parent="SoundArea3"] self_modulate = Color(0.827451, 0.345098, 0.0941176, 1) z_index = -1 position = Vector2(52.3218, 46.0544) scale = Vector2(3, 3) texture = ExtResource("2_mamb7") [node name="Label3" type="Label" parent="SoundArea3"] size_flags_stretch_ratio = 0.0 text = "File loaded as Music Start when entering Stop when exiting Only one instance of that music can be played " [node name="WelcomeOptionButton" type="OptionButton" parent="."] offset_left = 918.0 offset_top = 615.0 offset_right = 986.0 offset_bottom = 646.0 item_count = 3 popup/item_0/text = "CN" popup/item_0/id = 0 popup/item_1/text = "EN" popup/item_1/id = 1 popup/item_2/text = "JP" popup/item_2/id = 2 script = ExtResource("8_f6f5a") [node name="WelcomeButton" type="Button" parent="."] offset_left = 987.0 offset_top = 615.0 offset_right = 1100.0 offset_bottom = 646.0 text = "Say welcome" script = ExtResource("9_jo4tj") welcome_option_button_path = NodePath("../WelcomeOptionButton") ================================================ FILE: demo/low_level_2D/FmodTest.gd ================================================ extends Node var master_string_bank: FmodBank var master_bank: FmodBank var music_bank: FmodBank var vehicles_bank: FmodBank var sfx_bank: FmodBank # Called when the node enters the scene tree for the first time. func _enter_tree(): # load banks # warning-ignore:return_value_discarded master_string_bank = FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded master_bank = FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded music_bank = FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded vehicles_bank = FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded sfx_bank = FmodServer.load_bank("res://assets/Banks/SFX.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) print("Fmod initialised.") ================================================ FILE: demo/low_level_2D/FmodTest.gd.uid ================================================ uid://dn0b4rle1712 ================================================ FILE: demo/low_level_2D/LangChooseButton.gd ================================================ class_name WelcomeOptionButton extends OptionButton var lang_bank: FmodBank func _enter_tree(): connect("item_selected", _on_item_selected) func _on_item_selected(index: int): if index == -1: lang_bank = null return var bank_path := "res://assets/Banks/Dialogue_%s.bank" % get_item_text(index) # warning-ignore:return_value_discarded lang_bank = FmodServer.load_bank(bank_path, FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ================================================ FILE: demo/low_level_2D/LangChooseButton.gd.uid ================================================ uid://bd3konietc7md ================================================ FILE: demo/low_level_2D/Listener.gd ================================================ extends CharacterBody2D var distance_traveled := 0 # Called when the node enters the scene tree for the first time. func _ready(): # register listener FmodServer.add_listener(0, self) print("Listener set.") return func _process(delta): var direction: Vector2 = Vector2(0,0) var rotation_dir = 0 if Input.is_action_pressed("right"): direction.x += 1 if Input.is_action_pressed("left"): direction.x -= 1 if Input.is_action_pressed("up"): direction.y -= 1 if Input.is_action_pressed("down"): direction.y += 1 if Input.is_action_pressed("rotate_right"): rotation_dir = 1 if Input.is_action_pressed("rotate_left"): rotation_dir = -1 if direction != Vector2(0,0): distance_traveled += delta * 200 if distance_traveled >= 35: distance_traveled = 0 FmodServer.play_one_shot("event:/Character/Player Footsteps") direction = direction.normalized() direction.x = direction.x * delta * 200 direction.y = direction.y * delta * 200 self.position += direction self.rotate(rotation_dir * delta * 5) if Input.is_action_pressed("lock_listener"): FmodServer.set_listener_lock(0, !FmodServer.get_listener_lock(0)) elif Input.is_action_pressed("kill"): self.queue_free() ================================================ FILE: demo/low_level_2D/Listener.gd.uid ================================================ uid://c4p1w0xlxjs4l ================================================ FILE: demo/low_level_2D/Listener2.gd ================================================ extends Node2D # Called when the node enters the scene tree for the first time. func _ready(): # register listener FmodServer.add_listener(1, self) print("Listener set.") return ================================================ FILE: demo/low_level_2D/Listener2.gd.uid ================================================ uid://luh7ddqnjxdb ================================================ FILE: demo/low_level_2D/WelcomeButton.gd ================================================ extends Button @export var welcome_option_button_path: NodePath var welcome_option_button: WelcomeOptionButton func _enter_tree(): connect("pressed", _on_pressed) func _ready(): welcome_option_button = get_node(welcome_option_button_path) func _on_pressed(): if welcome_option_button.lang_bank == null: return var event_instance = FmodServer.create_event_instance("event:/Character/Dialogue") event_instance.set_programmer_callback("welcome") event_instance.start() ================================================ FILE: demo/low_level_2D/WelcomeButton.gd.uid ================================================ uid://cpd21piypux6c ================================================ FILE: demo/project.godot ================================================ ; Engine configuration file. ; It's best edited using the editor UI and not directly, ; since the parameters that go here are not all obvious. ; ; Format: ; [section] ; section goes between [] ; param=value ; assign values to parameters config_version=5 [Fmod] "3D Settings/distance_factor"=64.0 General/banks_path="res://assets/Banks" [android] modules="org/godotengine/godot/FmodSingleton" [application] config/name="Fmod Demo" run/main_scene="res://low_level_2D/FmodScriptTest.tscn" config/features=PackedStringArray("4.5") config/icon="uid://cih2yjmtjoohg" [autoload] FmodManager="*res://addons/fmod/FmodManager.gd" [dotnet] project/assembly_name="Fmod Demo" [editor_plugins] enabled=PackedStringArray("res://addons/fmod/plugin.cfg", "res://addons/gut/plugin.cfg") [filesystem] import/blender/enabled=false [input] ui_accept={ "deadzone": 0.5, "events": [] } ui_select={ "deadzone": 0.5, "events": [] } ui_cancel={ "deadzone": 0.5, "events": [] } ui_focus_next={ "deadzone": 0.5, "events": [] } ui_focus_prev={ "deadzone": 0.5, "events": [] } ui_left={ "deadzone": 0.5, "events": [] } ui_right={ "deadzone": 0.5, "events": [] } ui_up={ "deadzone": 0.5, "events": [] } ui_down={ "deadzone": 0.5, "events": [] } ui_page_up={ "deadzone": 0.5, "events": [] } ui_page_down={ "deadzone": 0.5, "events": [] } ui_home={ "deadzone": 0.5, "events": [] } ui_end={ "deadzone": 0.5, "events": [] } right={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } left={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } down={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } space={ "deadzone": 0.5, "events": [null, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } kill={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } lock_listener={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } rotate_left={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } rotate_right={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } kill_event={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } up={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } engine_power_up={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } engine_power_down={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } [physics] 3d/default_gravity=98.0 [rendering] textures/vram_compression/import_etc2_astc=true ================================================ FILE: demo/run_tests.sh ================================================ if [ -z "$1" ]; then echo "ERROR: Please provide the path to the Godot executable as the first argument." exit 1 fi GODOT_BIN="$1" PROJECT_PATH="$PWD" DELAY_SEC=5 MAX_ATTEMPTS=3 retry_import() { for (( attempt=1; attempt<=MAX_ATTEMPTS; attempt++ )); do echo "INFO: Attempt $attempt/$MAX_ATTEMPTS: waiting $DELAY_SEC before import…" sleep "$DELAY_SEC" if "$GODOT_BIN" --headless --path "$PROJECT_PATH" --import; then echo "INFO: Import succeeded on attempt $attempt." return 0 else echo "WARN: Import failed on attempt $attempt. Retrying…" fi done return 1 } # Pre-import the project (headless). Try --import first, then fall back to headless editor. if ! retry_import; then echo "INFO: All --import attempts failed. Falling back to headless editor import…" sleep "$DELAY_SEC" if ! "$GODOT_BIN" --headless --path "$PROJECT_PATH" --editor --quit; then echo "WARNING: Fallback import (--editor --quit) failed. Continuing anyway…" fi fi "$GODOT_BIN" -s --headless --path "$PROJECT_PATH" addons/gut/gut_cmdln.gd | ( tests=0 passing=0 while read -r line do echo "$line" # Match line that starts with "Tests" if echo "$line" | grep -q "^Tests"; then tests=$(echo "$line" | awk '{print $NF}') fi # Match line that starts with "Passing Tests" if echo "$line" | grep -q "^Passing Tests"; then passing=$(echo "$line" | awk '{print $NF}') fi done echo "Retrieved values: tests=$tests passing=$passing" if [[ "$tests" -eq 0 ]]; then echo "ERROR: No tests were found." exit 1 fi if [[ "$tests" -eq "$passing" ]]; then exit 0 else echo "ERROR: Some assertions failed!" exit 1 fi ) result=$? # Exit with the result of the subshell exit $result ================================================ FILE: demo/test/integration/init ================================================ ================================================ FILE: demo/test/tests.tscn ================================================ [gd_scene format=3 uid="uid://cmtd8wjw7vfon"] [node name="Control" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 ================================================ FILE: demo/test/unit/test_bank.gd ================================================ extends "res://addons/gut/test.gd" class TestBank: extends "res://addons/gut/test.gd" var sprite: Sprite2D = Sprite2D.new() var master_strings_bank: FmodBank var masterBank: FmodBank var musicBank: FmodBank var vehicleBank: FmodBank func before_all(): # load banks # warning-ignore:return_value_discarded master_strings_bank = FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded masterBank = FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded musicBank = FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # warning-ignore:return_value_discarded vehicleBank = FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) var desired_value = FmodServer.FMOD_STUDIO_LOADING_STATE_LOADED assert_eq(vehicleBank.get_loading_state(), desired_value, "Loading state should be FMOD_STUDIO_LOADING_STATE_LOADED") FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): FmodServer.remove_listener(0, sprite) func test_assert_bank_bus_count(): var desiredValue: int = 0 assert_eq(musicBank.get_bus_count(), desiredValue, "Music bank should have " + str(desiredValue) + " buses") desiredValue = 12 assert_eq(masterBank.get_bus_count(), desiredValue, "Master bank should have " + str(desiredValue) + " buses") desiredValue = 0 assert_eq(vehicleBank.get_bus_count(), desiredValue, "Vehicles bank should have " + str(desiredValue) + " buses") func test_assert_bank_event_count(): var desiredValue: int = 4 assert_eq(musicBank.get_event_description_count(), desiredValue, "Music bank should have " + str(desiredValue) + " events") desiredValue = 5 assert_eq(masterBank.get_event_description_count(), desiredValue, "Master bank should have " + str(desiredValue) + " events") desiredValue = 2 assert_eq(vehicleBank.get_event_description_count(), desiredValue, "Vehicles bank should have " + str(desiredValue) + " events") func test_assert_bank_string_count(): var desiredValue: int = 0 assert_eq(musicBank.get_string_count(), desiredValue, "Music bank should have " + str(desiredValue) + " strings") assert_eq(masterBank.get_string_count(), desiredValue, "Master bank should have " + str(desiredValue) + " strings") assert_eq(vehicleBank.get_string_count(), desiredValue, "Vehicles bank should have " + str(desiredValue) + " strings") func test_assert_bank_vca_count(): var desiredValue: int = 0 assert_eq(musicBank.get_VCA_count(), desiredValue, "Music bank should have " + str(desiredValue) + " VCAs") desiredValue = 3 assert_eq(masterBank.get_VCA_count(), desiredValue, "Master bank should have " + str(desiredValue) + " VCAs") desiredValue = 0 assert_eq(vehicleBank.get_VCA_count(), desiredValue, "Vehicles bank should have " + str(desiredValue) + " VCAs") ================================================ FILE: demo/test/unit/test_bank.gd.uid ================================================ uid://bttd8qhpwy7c5 ================================================ FILE: demo/test/unit/test_bus.gd ================================================ extends "res://addons/gut/test.gd" class TestBus: extends "res://addons/gut/test.gd" var fmodEvent: FmodEvent var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) fmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): fmodEvent.release() FmodServer.remove_listener(0, sprite) func test_assert_should_has_master_bus(): var wanted: String = "bus:/" assert_true(FmodServer.check_bus_path(wanted), wanted + " should be present") func test_assert_should_has_master_bus_using_guid(): var wanted = "{af9d027a-3a1f-49a8-a9ef-4cbe20673632}" assert_true(FmodServer.check_bus_guid(wanted), wanted + " should be present") func test_assert_should_not_has_bus(): var wanted: String = "undefined" assert_false(FmodServer.check_bus_path(wanted), wanted + " should not be present") func test_assert_should_not_has_bus_using_guid(): var wanted = "{29583fe9-eee9-4c67-94e1-57d5f6c552af}" assert_false(FmodServer.check_bus_guid(wanted), wanted + " should not be present") func test_assert_mute_unmute(): var masterBus: FmodBus = FmodServer.get_bus("bus:/") assert_false(masterBus.mute, "Master bus should not be muted") masterBus.mute = true assert_true(masterBus.mute, "Master bus should be muted") masterBus.mute = false assert_false(masterBus.mute, "Master bus should not be muted") func test_assert_pause_unpause(): var masterBus: FmodBus = FmodServer.get_bus("bus:/") assert_false(masterBus.paused, "Master bus should not be paused") masterBus.paused = true assert_true(masterBus.paused, "Master bus should be paused") masterBus.paused = false assert_false(masterBus.paused, "Master bus should not be paused") func test_assert_volume(): _test_assert_volume(false) _test_assert_volume(true) func _test_assert_volume(is_guid: bool): var masterBus: FmodBus = FmodServer.get_bus_from_guid("{af9d027a-3a1f-49a8-a9ef-4cbe20673632}") if is_guid else FmodServer.get_bus("bus:/") var desiredValue: float = 1.0 assert_eq(masterBus.volume, desiredValue, "Bus volume should be " + str(desiredValue)) desiredValue = 0.5 masterBus.volume = desiredValue assert_eq(masterBus.volume, desiredValue, "Bus volume should be " + str(desiredValue)) masterBus.volume = 1 func test_assert_bus_stop_events(): var fmodEvent2: FmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") fmodEvent.start() fmodEvent2.start() await wait_seconds(2) assert_eq(fmodEvent.get_playback_state(), FmodServer.FMOD_STUDIO_PLAYBACK_PLAYING, "Event " + str(fmodEvent) + " playback state should be " + str(FmodServer.FMOD_STUDIO_PLAYBACK_PLAYING)) assert_eq(fmodEvent2.get_playback_state(), FmodServer.FMOD_STUDIO_PLAYBACK_PLAYING, "Event " + str(fmodEvent2) + " playback state should be " + str(FmodServer.FMOD_STUDIO_PLAYBACK_PLAYING)) FmodServer.get_bus("bus:/").stop_all_events(FmodServer.FMOD_STUDIO_STOP_IMMEDIATE) await wait_seconds(2) assert_eq(fmodEvent.get_playback_state(), FmodServer.FMOD_STUDIO_PLAYBACK_STOPPED, "Event " + str(fmodEvent) + " playback state should be " + str(FmodServer.FMOD_STUDIO_PLAYBACK_STOPPED)) assert_eq(fmodEvent2.get_playback_state(), FmodServer.FMOD_STUDIO_PLAYBACK_STOPPED, "Event " + str(fmodEvent2) + " playback state should be " + str(FmodServer.FMOD_STUDIO_PLAYBACK_STOPPED)) fmodEvent2.release() ================================================ FILE: demo/test/unit/test_bus.gd.uid ================================================ uid://dbh20bblqx6ah ================================================ FILE: demo/test/unit/test_callbacks.gd ================================================ extends "res://addons/gut/test.gd" var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): FmodServer.remove_listener(0, sprite) func test_assert_has_signals(): var emitter: FmodEventEmitter2D = FmodEventEmitter2D.new() assert_has_signal(emitter, "timeline_beat") assert_has_signal(emitter, "timeline_marker") assert_has_signal(emitter, "start_failed") assert_has_signal(emitter, "started") assert_has_signal(emitter, "restarted") assert_has_signal(emitter, "stopped") emitter.free() var callback_called = false func on_callback(_dict: Dictionary, type: int): callback_called = true func test_assert_set_callback(): watch_signals(FmodServer) var fmod_event: FmodEvent = FmodServer.create_event_instance("event:/Music/Level 02") fmod_event.volume = 0 fmod_event.start() fmod_event.set_callback(on_callback, FmodServer.FMOD_STUDIO_EVENT_CALLBACK_ALL) await wait_seconds(2) assert_true(callback_called) fmod_event.stop(FmodServer.FMOD_STUDIO_STOP_IMMEDIATE) fmod_event.release() ================================================ FILE: demo/test/unit/test_callbacks.gd.uid ================================================ uid://dcq4k3m170y4f ================================================ FILE: demo/test/unit/test_desc_event.gd ================================================ extends "res://addons/gut/test.gd" class TestEventDescription: extends "res://addons/gut/test.gd" var fmodEvent: FmodEvent var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) fmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") func after_all(): fmodEvent.release() FmodServer.remove_listener(0, sprite) func test_assert_should_create_and_release(): var desired_value: int = 2 var fmodEvent2: FmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") var instance_list: Array = FmodServer.get_event("event:/Vehicles/Car Engine").get_instance_list() assert_eq(instance_list.size(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent2.release() desired_value = 1 await wait_seconds(2) assert_eq(FmodServer.get_event("event:/Vehicles/Car Engine").get_instance_list().size(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent2 = FmodServer.create_event_instance("event:/Vehicles/Car Engine") var fmodEvent3: FmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") desired_value = 3 assert_eq(FmodServer.get_event("event:/Vehicles/Car Engine").get_instance_list().size(), desired_value, "Event description list size should be " + str(desired_value)) FmodServer.get_event("event:/Vehicles/Car Engine").release_all_instances() desired_value = 0 await wait_seconds(2) assert_eq(FmodServer.get_event("event:/Vehicles/Car Engine").get_instance_count(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") func test_assert_should_create_and_release_using_guid(): var desired_value: int = 2 var fmodEvent2: FmodEvent = FmodServer.create_event_instance_with_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}") var instance_list: Array = FmodServer.get_event_from_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}").get_instance_list() assert_eq(instance_list.size(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent2.release() desired_value = 1 await wait_seconds(2) assert_eq(FmodServer.get_event("event:/Vehicles/Car Engine").get_instance_list().size(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent2 = FmodServer.create_event_instance_with_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}") var fmodEvent3: FmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") desired_value = 3 assert_eq(FmodServer.get_event_from_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}").get_instance_list().size(), desired_value, "Event description list size should be " + str(desired_value)) FmodServer.get_event_from_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}").release_all_instances() desired_value = 0 await wait_seconds(2) assert_eq(FmodServer.get_event_from_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}").get_instance_count(), desired_value, "Event description list size should be " + str(desired_value)) fmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") func test_assert_should_be_3d(): _test_assert_should_be_3d(false) _test_assert_should_be_3d(true) func _test_assert_should_be_3d(is_guid: bool): var event: FmodEventDescription = FmodServer.get_event_from_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}") if is_guid else FmodServer.get_event("event:/Vehicles/Car Engine") assert_true(event.is_3d(), "Event description should be 3D") func test_assert_should_not_be_oneshot(): assert_false(FmodServer.get_event("event:/Vehicles/Car Engine").is_one_shot(), "Event description should not be oneshot") func test_assert_should_not_be_snapshot(): assert_false(FmodServer.get_event("event:/Vehicles/Car Engine").is_snapshot(), "Event description should not be snapshot") func test_assert_should_not_be_stream(): assert_false(FmodServer.get_event("event:/Vehicles/Car Engine").is_stream(), "Event description should not be stream") func test_assert_should_not_have_cue(): assert_false(FmodServer.get_event("event:/Vehicles/Car Engine").has_sustain_point(), "Event description should not have cue") func test_assert_min_max_distance(): var desiredMin: float = 1.0 var desiredMax: float = 20.0 var minMaxDistance = FmodServer.get_event("event:/Vehicles/Car Engine").get_min_max_distance() assert_eq(minMaxDistance[0], desiredMin, "Event description minimum distance should be " + str(desiredMin)) assert_eq(minMaxDistance[1], desiredMax, "Event description maximum distance should be " + str(desiredMax)) func test_assert_sound_size(): var desiredValue: float = 2.0 assert_eq(FmodServer.get_event("event:/Vehicles/Car Engine").get_sound_size(), desiredValue, "Event description sound size should be " + str(desiredValue)) func test_assert_should_retrieve_user_property_by_name(): var desiredSize: int = 0 var property = FmodServer.get_event("event:/Vehicles/Car Engine").get_user_property("abc") assert_eq(property.size(), desiredSize, "Number of user properties should be " + str(desiredSize)) func test_assert_should_retrieve_user_property_by_index(): var desiredSize: int = 0 var property = FmodServer.get_event("event:/Vehicles/Car Engine").user_property_by_index(0) assert_eq(property.size(), desiredSize, "Number of user properties should be " + str(desiredSize)) func test_assert_should_retrieve_user_property_count(): var desiredSize: int = 0 var property = FmodServer.get_event("event:/Vehicles/Car Engine").get_user_property_count() assert_eq(property, desiredSize, "Number of user properties should be " + str(desiredSize)) ================================================ FILE: demo/test/unit/test_desc_event.gd.uid ================================================ uid://jqjegp14uulq ================================================ FILE: demo/test/unit/test_event.gd ================================================ extends "res://addons/gut/test.gd" class TestEvent: extends "res://addons/gut/test.gd" var fmodEvent: FmodEvent var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) fmodEvent = FmodServer.create_event_instance("event:/Vehicles/Car Engine") fmodEvent.set_parameter_by_name("RPM", 600) fmodEvent.start() func after_all(): fmodEvent.stop(FmodServer.FMOD_STUDIO_STOP_IMMEDIATE) fmodEvent.release() FmodServer.remove_listener(0, sprite) func test_should_has_event(): var wanted: String = "event:/Vehicles/Car Engine" assert_true(FmodServer.check_event_path(wanted), wanted + " should be present") func test_should_has_event_guid(): var wanted: String = "{0c8363b4-23af-4f9c-af4b-0951bfd37d84}" assert_true(FmodServer.check_event_guid(wanted), wanted + " should be present") func test_should_not_has_event(): var wanted: String = "undefined" assert_false(FmodServer.check_event_path(wanted), wanted + " should not be present") func test_should_not_has_event_guid(): var wanted: String = "{29583fe9-eee9-4c67-94e1-57d5f6c552af}" assert_false(FmodServer.check_event_guid(wanted), wanted + " should not be present") func test_assert_set_volume(): var desired_value: float = 4.0 fmodEvent.volume = desired_value assert_eq(fmodEvent.volume, desired_value, "Event volume should be 4") func test_assert_set_pitch(): var desired_value: float = 0.75 fmodEvent.pitch = desired_value assert_eq(fmodEvent.pitch, desired_value, "Event pitch should be 0.75") func test_assert_paused(): fmodEvent.paused = true assert_true(fmodEvent.paused, "Event should be paused") func test_assert_timeline_position(): var desired_value: int = 10 fmodEvent.paused = true fmodEvent.position = desired_value await wait_seconds(2) assert_eq(fmodEvent.position, desired_value, "Event timeline should be at " + str(desired_value)) func test_assert_event_reverb(): var desired_value: float = 1.5 fmodEvent.set_reverb_level(0, desired_value) assert_eq(fmodEvent.get_reverb_level(0), desired_value, "Event reverb level should be " + str(desired_value)) func test_assert_event_parameter_by_name(): var desired_value: float = 600.0 assert_eq(fmodEvent.get_parameter_by_name("RPM"), desired_value, "Event parameter RPM should be " + str(desired_value)) func test_assert_should_pause_all(): fmodEvent.paused = false var fmodEvent2 = FmodServer.create_event_instance_with_guid("{0c8363b4-23af-4f9c-af4b-0951bfd37d84}") fmodEvent2.start() FmodServer.pause_all_events() assert_true(fmodEvent.paused, "Event " + str(fmodEvent) + " should be paused") assert_true(fmodEvent2.paused, "Event " + str(fmodEvent2) + " should be paused") fmodEvent2.stop(FmodServer.FMOD_STUDIO_STOP_IMMEDIATE) fmodEvent2.release() func test_assert_should_mute_unmute_all(): FmodServer.mute_all_events() var bus = FmodServer.get_bus("bus:/") assert_true(bus.mute, "Master bus should be muted") FmodServer.unmute_all_events() assert_false(bus.mute, "Master bus should not be muted") ================================================ FILE: demo/test/unit/test_event.gd.uid ================================================ uid://mh5ug8qwcjhx ================================================ FILE: demo/test/unit/test_global.gd ================================================ extends "res://addons/gut/test.gd" class TestGlobal: extends "res://addons/gut/test.gd" var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): FmodServer.remove_listener(0, sprite) func test_assert_should_have_performance_data(): var perf_data: FmodPerformanceData = FmodServer.get_performance_data() assert_not_null(perf_data, "Performance data should not be null.") func test_assert_should_have_dsp_buffer_length(): var buffer_length = FmodServer.get_system_dsp_buffer_length() assert_eq(buffer_length, 512) func test_assert_should_have_dsp_num_buffers(): var num_buffers = FmodServer.get_system_dsp_num_buffers() assert_eq(num_buffers, 4) func test_assert_should_have_dsp_buffer_size(): var buffer_size: FmodDspSettings = FmodServer.get_system_dsp_buffer_settings() assert_eq(buffer_size.dsp_buffer_size, 512) assert_eq(buffer_size.dsp_buffer_count, 4) ================================================ FILE: demo/test/unit/test_global.gd.uid ================================================ uid://1vqim544mnbm ================================================ FILE: demo/test/unit/test_listener.gd ================================================ extends "res://addons/gut/test.gd" class TestListener: extends "res://addons/gut/test.gd" var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): FmodServer.remove_listener(0, sprite) func test_assert_should_set_listener_num(): var desiredValue: int = 1 assert_listener_num(desiredValue) FmodServer.set_listener_number(2) desiredValue = 2 assert_listener_num(desiredValue) desiredValue = 1 FmodServer.set_listener_number(1) assert_listener_num(desiredValue) func test_assert_should_have_proper_weight(): var desiredValue: float = 1.0 assert_listener_weight(0, desiredValue) desiredValue = 2.0 FmodServer.set_listener_weight(0, desiredValue) assert_listener_weight(0, desiredValue) desiredValue = 1.0 FmodServer.set_listener_weight(0, desiredValue) FmodServer.set_listener_number(2) desiredValue = 2.0 FmodServer.add_listener(1, sprite) FmodServer.set_listener_weight(1, desiredValue) assert_listener_weight(1, desiredValue) FmodServer.remove_listener(1, sprite) FmodServer.set_listener_number(1) func test_assert_attach_object_to_listener(): var desired_listener: int = 0; var node_instance: Object = FmodServer.get_object_attached_to_listener(desired_listener) assert_false(node_instance == null, "Listener " + str(desired_listener) + " should have an object attached") FmodServer.set_listener_number(2); desired_listener = 1; assert_no_object_attached_to_listener(desired_listener) FmodServer.add_listener(desired_listener, sprite) assert_true(FmodServer.get_object_attached_to_listener(desired_listener) == node_instance, "Both listeners should be attached to same object") FmodServer.remove_listener(1, sprite) assert_no_object_attached_to_listener(desired_listener) FmodServer.set_listener_number(1) func test_attach_two_object_to_listeners(): var desired_listener := 1 FmodServer.set_listener_number(2); FmodServer.add_listener(desired_listener, sprite) assert_eq(FmodServer.get_object_attached_to_listener(desired_listener), sprite) var node := Node2D.new() FmodServer.add_listener(desired_listener, node) assert_eq(FmodServer.get_object_attached_to_listener(desired_listener), node) FmodServer.remove_listener(desired_listener, sprite) assert_eq(FmodServer.get_object_attached_to_listener(desired_listener), node) FmodServer.remove_listener(desired_listener, node) assert_no_object_attached_to_listener(desired_listener) node.free() FmodServer.set_listener_number(1); func assert_listener_num(desiredValue: int): assert_eq(FmodServer.get_listener_number(), desiredValue, "There should be " + str(desiredValue) + " listeners.") func assert_listener_weight(listenerNum: int, desiredValue: float): assert_eq(FmodServer.get_listener_weight(listenerNum), desiredValue, str(listenerNum) + " should have a weight of " + str(desiredValue)) func assert_no_object_attached_to_listener(desired_listener: int): assert_true(FmodServer.get_object_attached_to_listener(desired_listener) == null, "Listener " + str(desired_listener) + " should not have any object attached") ================================================ FILE: demo/test/unit/test_listener.gd.uid ================================================ uid://dptymuaf8rbid ================================================ FILE: demo/test/unit/test_sound.gd ================================================ extends "res://addons/gut/test.gd" class TestSound: extends "res://addons/gut/test.gd" var sound: FmodSound var music: FmodSound var sound_file = "res://assets/Sounds/doorOpen_1.ogg" var music_file = "res://assets/Music/jingles_SAX07.ogg" func before_all(): FmodServer.load_file_as_sound(sound_file) FmodServer.load_file_as_music(music_file) sound = FmodServer.create_sound_instance(sound_file) music = FmodServer.create_sound_instance(music_file) func after_all(): sound.release() music.release() FmodServer.unload_file(sound_file) FmodServer.unload_file(music_file) func test_assert_set_volume(): var desired_value: float = 2.0 sound.volume = desired_value assert_eq(sound.volume, desired_value, "Sound volume should be 2") music.volume = desired_value assert_eq(music.volume, desired_value, "Sound volume should be 2") func test_assert_set_pitch(): var desired_value: float = 0.75 sound.pitch = desired_value assert_eq(sound.pitch, desired_value, "Sound pitch should be 0.75") music.pitch = desired_value assert_eq(music.pitch, desired_value, "Sound pitch should be 0.75") func test_assert_playing(): sound.play() assert_true(sound.is_playing(), "Sound should be playing") music.play() assert_true(music.is_playing(), "Sound should be playing") ================================================ FILE: demo/test/unit/test_sound.gd.uid ================================================ uid://bwtkh2jv2lk6w ================================================ FILE: demo/test/unit/test_vca.gd ================================================ extends "res://addons/gut/test.gd" class TestVCA: extends "res://addons/gut/test.gd" var sprite: Sprite2D = Sprite2D.new() var banks := Array() func before_all(): # load banks # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) # warning-ignore:return_value_discarded banks.append( FmodServer.load_bank("res://assets/Banks/Vehicles.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) ) FmodServer.set_listener_number(1) get_tree().get_root().add_child(sprite) FmodServer.add_listener(0, sprite) func after_all(): FmodServer.remove_listener(0, sprite) func test_assert_valid_paths(): assert_true(FmodServer.check_vca_path("vca:/Environment"), "vca:/Environment should be present") assert_true(FmodServer.check_vca_path("vca:/Player"), "vca:/Player should be present") assert_true(FmodServer.check_vca_path("vca:/Equipment"), "vca:/Equipment should be present") func test_assert_invalid_path(): assert_false(FmodServer.check_vca_path("vca:/undefined"), "Invalid vca should not be present") func test_assert_volume(): _test_assert_volume(false) _test_assert_volume(true) func _test_assert_volume(is_guid: bool): var desired_value: float = 1.0 var environment_vca = FmodServer.get_vca_from_guid("{3f0b7d64-e765-400e-ae74-c2d973ad4ca1}") if is_guid else FmodServer.get_vca("vca:/Environment") assert_eq(environment_vca.volume, desired_value, "VCA volume should be " + str(desired_value)) desired_value = 0.5 environment_vca.volume = desired_value assert_eq(environment_vca.volume, desired_value, "VCA volume should be " + str(desired_value)) desired_value = 1.0 environment_vca.volume = desired_value ================================================ FILE: demo/test/unit/test_vca.gd.uid ================================================ uid://dfj4ym020lnkg ================================================ FILE: docs/.gitignore ================================================ site/ p3/ ================================================ FILE: docs/build.sh ================================================ #! /usr/bin/env bash set -e BASEDIR=$(dirname "$0") VIRTUALENV_DIR="$BASEDIR/p3" if [[ ! -d "$VIRTUALENV_DIR" ]]; then python -m pip install virtualenv python -m virtualenv -p python3 "$VIRTUALENV_DIR" fi source "$VIRTUALENV_DIR/bin/activate" pip install -r requirements.txt mkdocs build ================================================ FILE: docs/mkdocs.yml ================================================ site_name: Godot Fmod GdExtension docs_dir: src/doc repo_name: 'utopia-rise/fmod-gdextension' repo_url: 'https://github.com/utopia-rise/fmod-gdextension' theme: name: material favicon: assets/favicon.ico palette: - scheme: default toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode - scheme: slate toggle: icon: material/toggle-switch name: Switch to light mode logo: assets/fmod-gdextension-logo.png features: - navigation.tabs - navigation.sections - navigation.top extra: social: - icon: fontawesome/brands/github-alt link: https://github.com/utopia-rise markdown_extensions: - pymdownx.highlight: linenums: true - pymdownx.inlinehilite - pymdownx.highlight - pymdownx.superfences - pymdownx.tabbed - admonition - meta extra_javascript: - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js - js/init.js extra_css: - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css ================================================ FILE: docs/requirements.txt ================================================ mkdocs-material==7.1.0 mkdocs==1.3.0 ================================================ FILE: docs/run.sh ================================================ #! /usr/bin/env bash set -e BASEDIR=$(dirname "$0") VIRTUALENV_DIR="$BASEDIR/p3" if [[ ! -d "$VIRTUALENV_DIR" ]]; then python -m pip install virtualenv python -m virtualenv -p python3 "$VIRTUALENV_DIR" fi source "$VIRTUALENV_DIR/bin/activate" pip install -r requirements.txt mkdocs serve ================================================ FILE: docs/src/doc/advanced/1-compiling.md ================================================ # Compiling from sources Clone project with `--recurse_submodules`. ## Typical Project structure ``` └── Project root ├── libs | └── fmod | └── {platform} | └── specific platform fmod api goes here └── fmod-gdextension (this repo, consider using it as a submodule of you GDExtensions repo) ├── CMakeLists.txt (Here for CLion) ├── LICENSE ├── README.md ├── SConstruct (here to build on Windows, Linux, OSX and IOS ├── godot-cpp (gdextension bindings, submodule) └── src ``` You are supposed to put fmod libraries under `libs/fmod/{platform}`, according to the platforms you want to support. `libs` folder should be a brother to plugin repo in hierarchy. `CMakeLists` is here for CLion ide, as we are used to JetBrains tools. Unfortunately, CLion does not currently support Sconstruct. ## Compiling In order to compile you should run `scons` command, as for all gdextension projects. ``` scons platform= target= --jobs= ``` If `target` is `editor` or `template_debug`, debug symbols will be generated. ### Additional Instructions On Windows, you need to pass the `arch` argument with values `x86_64` or `x86_32`. Example: ``` scons platform=windows arch=x86_64 target=editor ``` You can also specify the FMOD directory as an argument, so you don't need to place the `libs` outside the project root. Example: ``` scons platform=windows arch=x86_64 target=editor fmod_lib_dir=libs/ ``` ================================================ FILE: docs/src/doc/index.md ================================================ [![GitHub](https://img.shields.io/github/license/utopia-rise/fmod-gdextension?style=flat-square)](LICENSE) ![fmod-gdextension-image] Godot C++ GDExtension for Godot 4 that provides an integration for the FMOD Studio API. FMOD is an audio engine and middleware solution for interactive audio in games. It has been the audio engine behind many titles such as Transistor, Into the Breach and Celeste. [More on FMOD's website](https://www.fmod.com/). This GDExtension exposes most of the Studio API functions to Godot's GDScript and also provides helpers ands nodes for performing common functions like attaching Studio events to Godot nodes and playing 3D/positional audio. Feel free to tweak/extend it based on your project's needs. [fmod-gdextension-image]: ./assets/fmod-gdextension-logo.png ================================================ FILE: docs/src/doc/user-guide/1-install.md ================================================ # Setup guide ## 1 - Install addon folder We provide releases in github repository. You can download `addon.zip` from this [page](https://github.com/utopia-rise/fmod-gdextension/releases), then unzip it and copy its content to the `addons` directory of your Godot project. ## 2 - Activate the plugin in Godot Open your Godot project, go to your project settings and select the `Plugins` tab. You should see the line `FMOD GDExtension` and a checkbox. Enable it. After this, you should be able to create Fmod nodes, use the bank explorer and write scripts using `FmodServer` ## 3 - Android specific Android exports require a Godot Android plugin. Download android build template from project menu: ![install-android-build-template] Make sure the `Use Gradle Build` option is activated in your Android export: ![android-extension-export-enable] !!! warning We curently only support armv8 architecture for android export. In order to get support for x86, you should build plugin on your own. [android-extension-export-enable]: ./assets/android-export-enable-extension.png [install-android-build-template]: ./assets/install-android-build-template.png ================================================ FILE: docs/src/doc/user-guide/2-initialization.md ================================================ # Initialization When you first add fmod addon to your godot project, you need to setup fmod options within projects parameters. Parameters are split in several categories: - General - Software Format - Dsp - 3d Settings ## General - "Auto Initialize": If `true`, will start fmod on engine startup. - "Channel Count": Maximum number of Channel objects available for playback, also known as virtual voices. Virtual voices will play with minimal overhead, with a subset of 'real' voices that are mixed, and selected based on priority and audibility. See the Virtual Voices guide for more information. - "Live update": Enable live update. - "Memory Tracking": Enables detailed memory usage statistics. Increases memory footprint and impacts performance. See Studio::Bus::getMemoryUsage and Studio::EventInstance::getMemoryUsage for more information. Implies FMOD_INIT_MEMORY_TRACKING. - Default Listener count: set max listener count (should be between 1 and 8). - Should Load by Name: If true will load events and parameters by name instead of id when using fmod nodes. ![general-tab] ## Software Format - "Sample Rate": Sample rate of the mixer. Range: [8000, 192000] Units: Hertz Default: 48000 - "Speaker Mode": Speaker setup of the mixer. [FMOD_SPEAKERMODE](https://www.fmod.com/docs/2.03/api/core-api-common.html#fmod_speakermode) - "Raw Speaker Count": Number of speakers for [FMOD_SPEAKERMODE_RAW](https://www.fmod.com/docs/2.03/api/core-api-common.html#fmod_speakermode_raw) mode. Range: [0, [FMOD_MAX_CHANNEL_WIDTH](https://www.fmod.com/docs/2.03/api/core-api-common.html#fmod_max_channel_width)] ![software-format-tab] ## Dsp - "Dsp Buffer Size": The mixer engine block size. Use this to adjust mixer update granularity. Units: Samples Default: 1024 - "Dsp Buffer Count": The mixer engine number of buffers used. Use this to adjust mixer latency. Default: 4 ![dsp-tab] ## 3d Settings - "Doppler Scale": A scaling factor for doppler shift. Default 1. - "Distance Factor": A factor for converting game distance units to FMOD distance units. Default 1. !!! warning In 2D this value represents pixels, so you should set it to the number of pixel for your world meter (If your world meter is 64px, set it to 64). In 3D this represents meter, so we recommend to set it to 1. - "Rolloff Scale": A scaling factor for distance attenuation. When a sound uses a roll-off mode other than FMOD_3D_CUSTOMROLLOFF and the distance is greater than the sound's minimum distance, the distance is scaled by the roll-off scale. ![3d-tab] ## Fmod explorer When all is setup you can explore your project's banks using `Fmod Explorer`. ![fmod-explorer] [general-tab]: ./assets/parameter-general.png [software-format-tab]: ./assets/parameters-software-format.png [dsp-tab]: ./assets/parameters-dsp.png [3d-tab]: ./assets/parameters-3d.png [fmod-explorer]: ./assets/fmod-explorer.png ================================================ FILE: docs/src/doc/user-guide/3-using-fmod-plugin.md ================================================ # Using FMOD plugin This documentation provides detailed guidance on how to use the FMOD GDExtension plugin for integrating FMOD into your Godot projects. The plugin offers two main approaches for interacting with FMOD: 1. **Using FMOD Nodes**: Predefined nodes that simplify common FMOD tasks. 2. **Using the `FmodServer` API**: A singleton that gives you direct access to FMOD's core and system APIs for more advanced or customized workflows. #### Core Component: `FmodServer` At the heart of the plugin is the `FmodServer` singleton. It serves as a bridge to FMOD's powerful API, allowing you to control audio behavior programmatically. FMOD nodes are built on top of this singleton, offering higher-level abstractions for convenience. ## Summary - Loading banks - [Using node](4-loading-banks.md#fmodbankloader-node) - [Using FmodServer](4-loading-banks.md#fmodserver-api) - Playing events - [Using node](5-playing-events.md#fmodeventemitter-nodes) - [Using FmodServer](5-playing-events.md#fmodserver-api) - Listeners - [Using node](6-listeners.md#fmod-listener-nodes) - [Using FmodServer](6-listeners.md#using-fmodserver-api) - [Playing sounds](7-playing-sounds.md) - [Other low level examples](8-other-low-level-examples.md) ================================================ FILE: docs/src/doc/user-guide/4-loading-banks.md ================================================ # Loading banks In this guide we'll explore how to load banks within your game. ## FmodBankLoader node `FmodBankLoader` is in charge of loading banks when entering the scene. You should place it, in the scene hierarchy, before all other fmod nodes using this bank. Banks are unloaded on exit tree. Banks are `RefCounted`, so several `FmodBankLoader` can share same banks. If you want to load your bank when starting game and keep them loaded, use this node and add it as **autoload** node. You can add banks with fmod project explorer, using the `+` button with bank icon, or manually add a bank using bottom line edit. You can also remove and re-order banks: ![fmod-bank-image] !!! warning Make sure to first place `Master.strings.bank` first, and `Master.bank` in second. Those banks are dependencies needed by other banks. So if you don't load them first, you won't be able to load other banks. ## FmodServer api You can also load banks using `FmodServer` api. For this purpose, you should use `load_bank` method of `FmodServer` singleton. Here is an example: ```gdscript var banks := Array() func _ready(): banks.append(FmodServer.load_bank("res://Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)) banks.append(FmodServer.load_bank("res://Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)) banks.append(FmodServer.load_bank("res://Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL)) ``` !!! warning As banks are `RefCounted`, don't forget to store them. Otherwise reference counter will be directly decremented leading to unload of the bank. !!! warning Make sure to first load `Master.strings.bank`, and `Master.bank` in second. Those banks are dependencies needed by other banks. So if you don't load them first, you won't be able to load other banks. [fmod-bank-image]: ./assets/fmod-bank.png ================================================ FILE: docs/src/doc/user-guide/5-playing-events.md ================================================ # Playing events In this guide we'll explore how to play Fmod events. ## FmodEventEmitter nodes `FmodEventEmitter2D` and `FmodEventEmitter3D` are nodes which play Fmod events. Events and their parameters are loaded by id or name according to [Fmod General settings](./2-initialization.md#general). ![emitter-image] ### Properties First, set the event that the emitter will play. You can set the name or guid of the event manually, or use the event selection button. Then, there are a few options you can toggle: - *attached*: if `true`, the Fmod event's position will update alongside the node's position. - *autoplay*: if `true`, the event will autoplay (if `false`, it will play when the `play()` function is called on it). - *auto_release*: if `true`, the emitter node will be automatically freed when the event finishes playing. - *allow_fadeout*: if `true`, the event will fade out when stopped. - *preload_event*: if `true`, the event will be preloaded when the node is ready. #### Fmod parameters Event emitters have dynamics properties corresponding to fmod parameters associated with the current event. You can set their values like any other godot float property. From scripts, you can change them using `get` and `set` object's operators. Example: ```gdscript extends FmodEventEmitter2D func _process(_delta): if Input.is_action_pressed("engine_power_up"): self["fmod_parameters/RPM"] = self["fmod_parameters/RPM"] + 10 if Input.is_action_pressed("engine_power_down"): self["fmod_parameters/RPM"] = self["fmod_parameters/RPM"] - 10 ``` To easily retrieve fmod parameters properties path, you can use godot's `Copy Property Path` functionality. You can also use following methods: - `get_parameter`: Get the value of a parameter using its name. - `set_parameter`: Set the value of a parameter using its name. - `get_parameter_by_id`: Get the value of a parameter using its id. - `set_parameter_by_id`: Set the value of a parameter using its id. Example: ```gdscript emitter.set_parameter("RPM", 1000) emitter.set_parameter_by_id(5864137074015534804, 1000) emitter.get_parameter("RPM") emitter.get_parameter_by_id(5864137074015534804) ``` ### Signals `FmodEventEmitter2D` and `FmodEventEmitter3D` emits signals: #### timeline_beat Emitted on fmod event's timeline beat callback. Parameters (as dictionary): - `beat`: Beat number within bar (starting from 1). - `bar`: Bar number (starting from 1). - `tempo`: Current tempo in beats per minute. - `time_signature_upper`: Current time signature upper number (beats per bar). - `time_signature_lower`: Current time signature lower number (beat unit). - `position`: Position of the beat on the timeline in milliseconds. #### timeline_marker: Emitted when fmod event timeline passes a named marker. Parameters (as dictionary): - `name`: Marker name. - `position`: Position of the marker on the timeline in milliseconds. #### started Emitted when event starts and was not playing. No parameters. #### restart Emitted when event starts and was playing. No parameters. #### stopped Emitted when event has stopped. No parameters. ### Methods #### play Starts the event. *parameters:* - `restart_if_playing`: If true, will restart event if it is already playing. Default value: `true`. #### play_one_shot Starts a one shot instance of the event that will not be managed by the emitter. Useful for short SFX. #### stop Stops the event. #### Set event methods Both methods `set_event_name` and `set_event_guid` changes the event played by the emitter. Those methods stop and unload the current playing event. It also clear the current parameters values. If emitter option `preload_event` is true, it will load the new event. If emitter option `autoplay` is true, it will load and play the new event. #### set_volume This sets the emitter volume. If the emitter doe not have any loaded event yet, this will be applied to the future event. ### Programmers callbacks To use a programmer callback, you must first load the bank with the audio table used by your event containing the programmer callback. You can then use `set_programmer_callback` on the event to specify the key from audio table to use. ```gdscript var event_emitter = FmodEventEmitter2D.new() event_emitter.event_guid = "{9aa2ecc5-ea4b-4ebe-85c3-054b11b21dcd}" # event:/Character/Dialogue from sfx bank. event_emitter.autoplay = true event_emitter.set_programmer_callback("welcome") # welcome key from audio table in Dialogue_EN.bank, Dialogue_JP.bank and Dialogue_CN.bank. One of those bank should be loaded. add_child(event_emitter) ``` ## FmodServer api You can also use `FmodServer` api to play events. Here is an example script: ```gdscript extends Sprite2D var isPlaying: bool = true var event: FmodEvent = null # Called when the node enters the scene tree for the first time. func _ready(): event = FmodServer.create_event_instance("event:/Vehicles/Car Engine") event.set_2d_attributes(self.global_transform) event.set_parameter_by_name("RPM", 600) event.volume = 2 event.start() # warning-ignore:unused_argument func _process(_delta): if Input.is_action_just_pressed("space"): isPlaying = !isPlaying if(isPlaying): print("Mower playing") event.paused = false else: print("Mower paused") event.paused = true elif Input.is_action_just_pressed("kill_event"): self.queue_free() ``` In this script we create an instance of `FmodEvent` by calling `FmodServer`. We then set its position attribute to the node's transform, set its `RPM` parameter to `600` and its `volume` to `2`. We then start it. In the `_process` method, we pause the event if we press `space` action. Here is another with setting callback: ```gdscript extends Area2D var event: FmodEvent = null var icon: Sprite2D # Called when the node enters the scene tree for the first time. func _ready(): event = FmodServer.create_event_instance("event:/Music/Level 02") event.set_callback(Callable(self, "change_color"), FmodServer.FMOD_STUDIO_EVENT_CALLBACK_ALL) body_entered.connect(enter) body_exited.connect(leave) event.start() event.paused = true # warning-ignore:unused_argument func enter(_area): print("enter") event.paused = false # warning-ignore:unused_argument func leave(_area): print("leave") event.paused = true # warning-ignore:unused_argument func change_color(_dict: Dictionary, type: int): if type == FmodServer.FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT: $icon.self_modulate = Color(randf_range(0,1), randf_range(0,1), randf_range(0,1), 1) ``` In this script we create an `FmodEvent` and set a callback on it. Each time the event emits the callback we change the node's color. ### Programmers callback In order to play a programmer callback you must load the bank with audio table concerned by the programmer callback. then you can use `set_programmer_callback` to specify key from audio table to use. ```gdscript FmodServer.load_bank("res://assets/Banks/Dialogue_EN.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) var event_instance = FmodServer.create_event_instance("event:/Character/Dialogue") # event from sfx bank. event_instance.set_programmer_callback("welcome") # welcome key in audio table from Dialogue bank. event_instance.start() ``` [emitter-image]: ./assets/emitter.png ================================================ FILE: docs/src/doc/user-guide/6-listeners.md ================================================ # Listeners In this guide we'll cover how to add FMOD listeners to your godot project. ## Fmod listener nodes `FmodListener2D` and `FmodListener3D` are nodes to add a FMOD listener and bind it to them, this means position of the FMOD listener will match position of the `FmodListener` node. ![listener-image] ### Properties: #### listener_index Index of the listener. You can have up to 8 listener concurrently. #### is_locked If `true`, listener will not update its position according to node's position. #### weight Used to compute the relative contribution to the final sound. ## Using FmodServer api You can add and remove listeners using `FmodServer` api: ```gdscript func _ready(): FmodServer.add_listener(0, self) ``` In this script we set a listener with index `0` and attached it to the current node. ```gdscript func _ready(): FmodServer.remove_listener(0, self) ``` In this script we remove the listener with index `0` attached to the current node. You can also set the listener weight using `FmodServer`: ```gdscript FmodServer.set_system_listener_weight(0, 1.5) ``` In this script we set the weight of the listener with index `0` to `1.5`. [listener-image]: ./assets/listeners.png ================================================ FILE: docs/src/doc/user-guide/7-playing-sounds.md ================================================ # Playing sounds You can play sounds using `FmodServer` api. Here is an example: ```gdscript extends Area2D # Declare member variables here. Examples: # var a = 2 var music: FmodSound = null # Called when the node enters the scene tree for the first time. func _ready(): FmodServer.load_file_as_music("res://assets/Music/jingles_SAX07.ogg") # warning-ignore:return_value_discarded body_entered.connect(enter) # warning-ignore:return_value_discarded body_exited.connect(leave) # warning-ignore:unused_argument func enter(_area): print("enter") music = FmodServer.create_sound_instance("res://assets/Music/jingles_SAX07.ogg") music.play() # warning-ignore:unused_argument func leave(_area): print("leave") music.release() func _exit_tree(): FmodServer.unload_file("res://assets/Music/jingles_SAX07.ogg") ``` This script loads an ogg file. Then it connects godot's `body_entered` and `body_exited` signals. When we enter the node, it will create a sound instance using `create_sound_instance` method and play it. When we exit the node, it will release the sound. At the end, we the node is removed from godot tree, it unloads the sound file. ================================================ FILE: docs/src/doc/user-guide/8-other-low-level-examples.md ================================================ # Other FmodServer api examples ## Muting all event You can mute all event using `mute_all_events`. This will mute the master bus. ```gdscript func _ready(): FmodServer.set_software_format(0, FmodServer.FMOD_SPEAKERMODE_STEREO, 0) FmodServer.init(1024, FmodServer.FMOD_STUDIO_INIT_LIVEUPDATE, FmodServer.FMOD_INIT_NORMAL) FmodServer.set_sound_3d_settings(1.0, 64.0, 1.0) # load banks FmodServer.load_bank("res://Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) FmodServer.load_bank("res://Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) FmodServer.load_bank("res://Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # register listener FmodServer.add_listener(0, self) # play some events FmodServer.play_one_shot("event:/Music/Level 02", self) var my_music_event = FmodServer.create_event_instance("event:/Music/Level 01") FmodServer.start_event(my_music_event) var t = Timer.new() t.set_wait_time(3) t.set_one_shot(true) self.add_child(t) t.start() yield(t, "timeout") FmodServer.mute_all_events(); t = Timer.new() t.set_wait_time(3) t.set_one_shot(true) self.add_child(t) t.start() yield(t, "timeout") FmodServer.unmute_all_events() ``` ## Pausing all events ```gdscript func _ready(): # set up FMOD FmodServer.set_software_format(0, FmodServer.FMOD_SPEAKERMODE_STEREO, 0) FmodServer.init(1024, FmodServer.FMOD_STUDIO_INIT_LIVEUPDATE, FmodServer.FMOD_INIT_NORMAL) FmodServer.set_sound_3d_settings(1/0, 64.0, 1.0) # load banks FmodServer.load_bank("res://Master.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) FmodServer.load_bank("res://Master.strings.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) FmodServer.load_bank("res://Music.bank", FmodServer.FMOD_STUDIO_LOAD_BANK_NORMAL) # register listener FmodServer.add_listener(0, self) # play some events FmodServer.play_one_shot("event:/Music/Level 02", self) var my_music_event = FmodServer.create_event_instance("event:/Music/Level 01") FmodServer.start_event(my_music_event) var t = Timer.new() t.set_wait_time(3) t.set_one_shot(true) self.add_child(t) t.start() yield(t, "timeout") FmodServer.pause_all_events(true) t = Timer.new() t.set_wait_time(3) t.set_one_shot(true) self.add_child(t) t.start() yield(t, "timeout") FmodServer.pause_all_events(false) ``` ## Changing the default audio output device By default, FMOD will use the primary audio output device as determined by the operating system. This can be changed at runtime, ideally through your game's Options Menu. Here, `get_available_drivers()` returns an Array which contains a Dictionary for every audio driver found. Each Dictionary contains fields such as the name, sample rate and speaker config of the respective driver. Most importantly, it contains the id for that driver. ```gdscript # retrieve all available audio drivers var drivers = FmodServer.get_available_drivers() # change the audio driver # you must pass in the id of the respective driver FmodServer.set_driver(id) # retrieve the id of the currently set driver var id = FmodServer.get_driver() ``` ## Reducing audio playback latency You may encounter that the audio playback has some latency. This may be caused by the DSP buffer size. You can change the value **before** initialisation to adjust it: ```gdscript FmodServer.set_dsp_buffer_size(512, 4) # retrieve the buffer length FmodServer.get_dsp_buffer_length() # retrieve the number of buffers FmodServer.get_dsp_num_buffers() ``` ## Profiling & querying performance data `get_performance_data` returns an object which contains current performance stats for CPU, Memory and File Streaming usage of both FMOD Studio and the Core System. ```gdscript # called every frame var perf_data = FmodServer.get_performance_data() print(perf_data.CPU) print(perf_data.memory) print(perf_data.file) ``` ================================================ FILE: docs/src/doc/user-guide/9-plugins.md ================================================ # Plugins !!! warning This feature is experimental. Please report any bug you find. This addon supports FMOD plugins. First you'll need to add plugins libraries (`.dll`, `.dylib`, `.so`, `.a`) in a project's folder. In this documentation we will use `assets/plugins` as an example. In this directory you have to create one directory per os, like: ``` assets └───── plugins ├────── windows │ └────── .dll for windows goes here ├────── linux │ └────── .so for linux goes here ├────── macos │ └────── .dylib for macos goes here ├────── android │ ├────── x86_64 │ │ └────── .so for android x86 goes here │ └────── arm64 │ └────── .so for android arm64 goes here └────── ios └────── .a for ios goes here ``` If there are some dependent libraries needed for a plugin, just place it next to plugin's library, all libraries in those folders are exported. In order to add plugins you first need to create an `FmodPluginsSettings` resource in your project: ![plugins-create-settings-resource] First you need to configure the directory containing the plugins libraries. In this example `res://assets/plugins/` Then you have to configure the plugin lists. There is two lists to configure: - `Dynamic Plugin List` is used for all platforms except `iOS` - `Static Plugins Methods` is used for `iOS` only For dynamic plugins you have to enter the name of the library for the plugin (without the lib prefix). As an example for steam plugin it will be `phonon_fmod`. In static plugins methods you have to place the methods from the documentation of your plugin. As and example for steam plugin it will be: - `FMOD_SteamAudio_Spatialize_GetDSPDescription`, with DSP type - `FMOD_SteamAudio_MixerReturn_GetDSPDescription`, with DSP type - `FMOD_SteamAudio_Reverb_GetDSPDescription`, with DSP type Here is what the configuration looks like for this example: ![fmod-plugins-settings-resource] Then you have to set the path of this resource in project's settings, in the `Fmod/Plugins` section: ![plugins-project-settings] [plugins-create-settings-resource]: ./assets/plugins-create-settings-resource.png [fmod-plugins-settings-resource]: ./assets/fmod-plugins-settings-resource.png [plugins-project-settings]: ./assets/plugins-project-settings.png ================================================ FILE: get_fmod.py ================================================ import requests import sys argv = sys.argv[1:] user = argv[0] password = argv[1] platform = argv[2] fmod_version = argv[3] fmodlink = "https://www.fmod.com/api-login" if platform == 'linux': # linux filename = f'fmodstudioapi{fmod_version}linux.tar.gz' downloadlink = f'https://www.fmod.com/api-get-download-link?path=files/fmodstudio/api/Linux/&filename=fmodstudioapi{fmod_version}linux.tar.gz&user=' elif platform == 'macos': # OS X filename = f'fmodstudioapi{fmod_version}osx.dmg' downloadlink = f'https://www.fmod.com/api-get-download-link?path=files/fmodstudio/api/Mac/&filename=fmodstudioapi{fmod_version}mac-installer.dmg&user=' elif platform == 'windows': # Windows... filename = f'fmodstudioapi{fmod_version}win-installer.exe' downloadlink = f'https://www.fmod.com/api-get-download-link?path=files/fmodstudio/api/Windows/&filename=fmodstudioapi{fmod_version}win-installer.exe&user=' elif platform == 'android': # Android... filename = f'fmodstudioapi{fmod_version}android.tar.gz' downloadlink = f'https://www.fmod.com/api-get-download-link?path=files/fmodstudio/api/Android/&filename=fmodstudioapi{fmod_version}android.tar.gz&user=' elif platform == 'ios': # iOS... filename = f'fmodstudioapi{fmod_version}ios.dmg' downloadlink = f'https://www.fmod.com/api-get-download-link?path=files/fmodstudio/api/iOS/&filename=fmodstudioapi{fmod_version}ios-installer.dmg&user=$1' downloadlink += user # First login and get a token! response = requests.post(fmodlink, auth = (user, password)).json() token = response["token"] print("Received token from FMOD login API.") # Next request a download link using the token! response = requests.get(downloadlink, headers = {"Authorization": f"Bearer {token}"}).json() url = response["url"] # Download FMOD! response = requests.get(url, allow_redirects=True) open(filename, 'wb').write(response.content) print("Downloading FMOD using the requested download link.") print(response) ================================================ FILE: jni/Application.mk ================================================ # Application.mk APP_STL:=c++_shared APP_ABI:=arm64-v8a armeabi-v7a ================================================ FILE: src/callback/event_callbacks.cpp ================================================ #include "fmod_studio.hpp" #include #include "fmod_server.h" #include #include #include namespace Callbacks { FMOD_RESULT F_CALL event_callback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE* event, void* parameters) { auto* instance = reinterpret_cast(event); godot::FmodEvent* event_instance; instance->getUserData((void**) &event_instance); if (event_instance) { if (type == FMOD_STUDIO_EVENT_CALLBACK_CREATE_PROGRAMMER_SOUND) { const godot::String& sound_key {event_instance->get_programmers_callback_sound_key()}; FMOD_STUDIO_SOUND_INFO sound_info {godot::FmodServer::get_singleton()->get_sound_info(sound_key)}; FMOD::Sound* sound { godot::FmodServer::get_singleton()->create_sound(sound_info, FMOD_LOOP_NORMAL | FMOD_CREATECOMPRESSEDSAMPLE | FMOD_NONBLOCKING) }; auto* props { reinterpret_cast(parameters) }; props->sound = (FMOD_SOUND*) sound; props->subsoundIndex = sound_info.subsoundindex; return FMOD_OK; } if (type == FMOD_STUDIO_EVENT_CALLBACK_DESTROY_PROGRAMMER_SOUND) { auto* props { reinterpret_cast(parameters) }; auto* sound {(FMOD::Sound*) props->sound}; ERROR_CHECK(sound->release()); return FMOD_OK; } godot::Dictionary dictionary; if (type == FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER) { auto* props { reinterpret_cast(parameters) }; dictionary["name"] = props->name; dictionary["position"] = props->position; } else if (type == FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT) { auto* props { reinterpret_cast(parameters) }; dictionary["beat"] = props->beat; dictionary["bar"] = props->bar; dictionary["tempo"] = props->tempo; dictionary["time_signature_upper"] = props->timesignatureupper; dictionary["time_signature_lower"] = props->timesignaturelower; dictionary["position"] = props->position; } const godot::Callable& callback {event_instance->get_callback()}; if (!callback.is_null() && callback.is_valid()) { godot::FmodServer::get_singleton()->add_callback( { type, callback, dictionary } ); } } return FMOD_OK; } }// namespace Callbacks ================================================ FILE: src/callback/event_callbacks.h ================================================ #ifndef GODOTFMOD_GODOT_FMOD_CALLBACK_H #define GODOTFMOD_GODOT_FMOD_CALLBACK_H #include #include namespace Callbacks { FMOD_RESULT F_CALL event_callback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE* event, void* parameters); }// namespace Callbacks #endif// GODOTFMOD_GODOT_FMOD_CALLBACK_H ================================================ FILE: src/callback/file_callbacks.cpp ================================================ #include "file_callbacks.h" namespace Callbacks { GodotFileRunner* GodotFileRunner::get_singleton() { static GodotFileRunner singleton; return &singleton; } void GodotFileRunner::queueReadRequest(FMOD_ASYNCREADINFO* request, ReadPriority priority) { // High-priority requests have to be processed first. if (priority == ReadPriority::HIGH) { // lock so we can't add and remove elements from the queue at the same time. std::lock_guard lk(read_mut); requests.push_front(request); } else { // lock so we can't add and remove elements from the queue at the same time. std::lock_guard lk(read_mut); requests.push_back(request); } read_cv.notify_one(); } FMOD_RESULT GodotFileRunner::cancelReadRequest(FMOD_ASYNCREADINFO* request) { // lock so we can't add and remove elements from the queue at the same time. { std::lock_guard lk(read_mut); if (requests.erase(request)) { request->bytesread = 0; request->done(request, FMOD_RESULT::FMOD_ERR_FILE_DISKEJECTED); return FMOD_RESULT::FMOD_ERR_FILE_DISKEJECTED; } } // We lock and check if the current request is the one being canceled. // In this case, we wait until it's done. { std::unique_lock lk(cancel_mut); if (request == current_request) { cancel_cv.wait(lk); } } return FMOD_RESULT::FMOD_OK; } void GodotFileRunner::run() { while (!stop) { // waiting for the container to have one request { std::unique_lock lk(read_mut); read_cv.wait(lk, [this] { return !requests.is_empty() || stop; }); } while (!requests.is_empty()) { // lock so we can't add and remove elements from the queue at the same time. // also store the current request so it cannot be canceled during processing. { std::lock_guard lk(read_mut); current_request = requests.front()->get(); requests.pop_front(); } // We get the Godot File object from the handle GodotFileHandle* handle {reinterpret_cast(current_request->handle)}; godot::Ref file {handle->file}; // update the position of the cursor file->seek(current_request->offset); // We read and store the requested data in an array. godot::PackedByteArray buffer {file->get_buffer(current_request->sizebytes)}; int size {static_cast(buffer.size())}; const uint8_t* data {buffer.ptr()}; // We copy the data to FMOD buffer memcpy(current_request->buffer, data, size * sizeof(uint8_t)); current_request->bytesread = size; // Remember to return an error if the end of the file is reached FMOD_RESULT result = (size < current_request->sizebytes) ? FMOD_RESULT::FMOD_ERR_FILE_EOF : FMOD_RESULT::FMOD_OK; current_request->done(current_request, result); // Request no longer processed { std::lock_guard lk(cancel_mut); current_request = nullptr; } cancel_cv.notify_one(); } } } void GodotFileRunner::start() { stop = false; fileThread = std::thread(&GodotFileRunner::run, this); } void GodotFileRunner::finish() { stop = true; // we need to notify the loop one last time, otherwise it will stay stuck in the wait method. read_cv.notify_one(); fileThread.join(); } FMOD_RESULT F_CALL godotFileOpen(const char* name, unsigned int* filesize, void** handle, void* userdata) { godot::Ref access = godot::FileAccess::open(name, godot::FileAccess::ModeFlags::READ); if (access->get_error() == godot::Error::OK) { *filesize = access->get_length(); GodotFileHandle* fileHandle {new GodotFileHandle {access}}; *handle = reinterpret_cast(fileHandle); return FMOD_RESULT::FMOD_OK; } return FMOD_RESULT::FMOD_ERR_FILE_NOTFOUND; } FMOD_RESULT F_CALL godotFileClose(void* handle, void* userdata) { godot::Ref file {reinterpret_cast(handle)->file}; delete reinterpret_cast(handle); return FMOD_RESULT::FMOD_OK; } FMOD_RESULT F_CALL godotSyncRead(FMOD_ASYNCREADINFO* info, void* userdata) { GodotFileRunner* runner {GodotFileRunner::get_singleton()}; int priority {info->priority}; GodotFileRunner::ReadPriority priorityRank; if (priority >= 50) { priorityRank = GodotFileRunner::ReadPriority::HIGH; } else { priorityRank = GodotFileRunner::ReadPriority::NORMAL; } runner->queueReadRequest(info, priorityRank); return FMOD_RESULT::FMOD_OK; } FMOD_RESULT F_CALL godotSyncCancel(FMOD_ASYNCREADINFO* info, void* userdata) { GodotFileRunner* runner {GodotFileRunner::get_singleton()}; return runner->cancelReadRequest(info); } }// namespace Callbacks ================================================ FILE: src/callback/file_callbacks.h ================================================ #ifndef GODOTFMOD_FILE_CALLBACKS_H #define GODOTFMOD_FILE_CALLBACKS_H #include #include #include #include #include #include // This include is required for both Linux and MacOS targets as they don't include the necessary headers for 'memcpy' by default #include namespace Callbacks { struct GodotFileHandle { godot::Ref file; }; class GodotFileRunner { public: static GodotFileRunner* get_singleton(); enum ReadPriority { NORMAL, HIGH }; ~GodotFileRunner() = default; private: std::thread fileThread; std::condition_variable read_cv; std::mutex read_mut; std::condition_variable cancel_cv; std::mutex cancel_mut; bool stop = false; FMOD_ASYNCREADINFO* current_request = nullptr; godot::List requests = godot::List(); GodotFileRunner() = default; GodotFileRunner(const GodotFileRunner&) = delete; GodotFileRunner& operator=(const GodotFileRunner&) = delete; void run(); public: void queueReadRequest(FMOD_ASYNCREADINFO* request, ReadPriority priority); FMOD_RESULT cancelReadRequest(FMOD_ASYNCREADINFO* request); void start(); void finish(); }; FMOD_RESULT F_CALL godotFileOpen(const char* name, unsigned int* filesize, void** handle, void* userdata); FMOD_RESULT F_CALL godotFileClose(void* handle, void* userdata); FMOD_RESULT F_CALL godotSyncRead(FMOD_ASYNCREADINFO* info, void* userdata); FMOD_RESULT F_CALL godotSyncCancel(FMOD_ASYNCREADINFO* info, void* userdata); }// namespace Callbacks #endif// GODOTFMOD_FILE_CALLBACKS_H ================================================ FILE: src/constants.h ================================================ #ifndef GODOTFMOD_CONSTANTS_H #define GODOTFMOD_CONSTANTS_H static constexpr const char* FMOD_SETTINGS_BASE_PATH = "Fmod"; static constexpr const char* FMOD_SETTING_AUTO_INITIALIZE = "auto_initialize"; static constexpr const bool DEFAULT_AUTO_INITIALIZE = true; #endif// GODOTFMOD_CONSTANTS_H ================================================ FILE: src/core/fmod_file.cpp ================================================ #include "fmod_file.h" using namespace godot; void FmodFile::_bind_methods() {} ================================================ FILE: src/core/fmod_file.h ================================================ #ifndef GODOTFMOD_FMOD_FILE_H #define GODOTFMOD_FMOD_FILE_H #include "classes/ref_counted.hpp" #include "fmod.hpp" namespace godot { class FmodFile : public RefCounted { GDCLASS(FmodFile, RefCounted); FMOD::Sound* _wrapped = nullptr; public: inline static Ref create_ref(FMOD::Sound* wrapped) { Ref ref; if (wrapped) { ref.instantiate(); ref->_wrapped = wrapped; wrapped->setUserData(ref.ptr()); } return ref; } FMOD::Sound* get_wrapped() const { return _wrapped; } protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_FILE_H ================================================ FILE: src/core/fmod_sound.cpp ================================================ #include "fmod_sound.h" #include "helpers/common.h" using namespace godot; void FmodSound::_bind_methods() { ClassDB::bind_method(D_METHOD("is_valid"), &FmodSound::is_valid); ClassDB::bind_method(D_METHOD("release"), &FmodSound::release); ClassDB::bind_method(D_METHOD("play"), &FmodSound::play); ClassDB::bind_method(D_METHOD("stop"), &FmodSound::stop); ClassDB::bind_method(D_METHOD("set_paused", "paused"), &FmodSound::set_paused); ClassDB::bind_method(D_METHOD("is_playing"), &FmodSound::is_playing); ClassDB::bind_method(D_METHOD("set_volume", "volume"), &FmodSound::set_volume); ClassDB::bind_method(D_METHOD("get_volume"), &FmodSound::get_volume); ClassDB::bind_method(D_METHOD("set_pitch", "pitch"), &FmodSound::set_pitch); ClassDB::bind_method(D_METHOD("get_pitch"), &FmodSound::get_pitch); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_pitch", "get_pitch"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_volume", "get_volume"); } void FmodSound::release() const { ERROR_CHECK(_wrapped->stop()); } void FmodSound::play() const { set_paused(false); } void FmodSound::set_paused(bool paused) const { ERROR_CHECK(_wrapped->setPaused(paused)); } void FmodSound::stop() const { ERROR_CHECK(_wrapped->stop()); } bool FmodSound::is_playing() const { bool isPlaying = false; ERROR_CHECK(_wrapped->isPlaying(&isPlaying)); return isPlaying; } void FmodSound::set_volume(float volume) const { ERROR_CHECK(_wrapped->setVolume(volume)); } float FmodSound::get_volume() const { float volume = 0.f; ERROR_CHECK(_wrapped->getVolume(&volume)); return volume; } float FmodSound::get_pitch() const { float pitch = 0.f; ERROR_CHECK(_wrapped->getPitch(&pitch)); return pitch; } void FmodSound::set_pitch(float pitch) { ERROR_CHECK(_wrapped->setPitch(pitch)); } bool FmodSound::is_valid() const { bool isPlaying; FMOD_RESULT result = _wrapped->isPlaying(&isPlaying); return result != FMOD_ERR_INVALID_HANDLE; } ================================================ FILE: src/core/fmod_sound.h ================================================ #ifndef GODOTFMOD_FMOD_SOUND_H #define GODOTFMOD_FMOD_SOUND_H #include "classes/ref_counted.hpp" #include "fmod.hpp" namespace godot { class FmodSound : public RefCounted { GDCLASS(FmodSound, RefCounted); FMOD::Channel* _wrapped = nullptr; public: inline static Ref create_ref(FMOD::Channel* wrapped) { Ref ref; if (wrapped) { ref.instantiate(); ref->_wrapped = wrapped; wrapped->setUserData(ref.ptr()); } return ref; } void set_paused(bool paused) const; void stop() const; bool is_playing() const; void set_volume(float volume) const; float get_volume() const; float get_pitch() const; void set_pitch(float pitch); bool is_valid() const; void play() const; void release() const; protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_SOUND_H ================================================ FILE: src/data/performance_data.cpp ================================================ #include "performance_data.h" using namespace godot; void FmodPerformanceData::_bind_methods() { ClassDB::bind_method(D_METHOD("get_dsp"), &FmodPerformanceData::get_dsp); ClassDB::bind_method(D_METHOD("get_geometry"), &FmodPerformanceData::get_geometry); ClassDB::bind_method(D_METHOD("get_stream"), &FmodPerformanceData::get_stream); ClassDB::bind_method(D_METHOD("get_update"), &FmodPerformanceData::get_update); ClassDB::bind_method(D_METHOD("get_convolution1"), &FmodPerformanceData::get_convolution1); ClassDB::bind_method(D_METHOD("get_convolution2"), &FmodPerformanceData::get_convolution2); ClassDB::bind_method(D_METHOD("get_studio"), &FmodPerformanceData::get_studio); ClassDB::bind_method(D_METHOD("get_currently_allocated"), &FmodPerformanceData::get_currently_allocated); ClassDB::bind_method(D_METHOD("get_max_allocated"), &FmodPerformanceData::get_max_allocated); ClassDB::bind_method(D_METHOD("get_sample_bytes_read"), &FmodPerformanceData::get_sample_bytes_read); ClassDB::bind_method(D_METHOD("get_stream_bytes_read"), &FmodPerformanceData::get_stream_bytes_read); ClassDB::bind_method(D_METHOD("get_other_bytes_read"), &FmodPerformanceData::get_other_bytes_read); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "dsp", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_dsp"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "geometry", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_geometry"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "stream", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "update", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_update"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "convolution1", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_convolution1"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "convolution2", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_convolution2"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "studio", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_studio"); ADD_PROPERTY(PropertyInfo(Variant::INT, "currently_allocated", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_currently_allocated"); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_allocated", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_max_allocated"); ADD_PROPERTY(PropertyInfo(Variant::INT, "sample_bytes_read", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_sample_bytes_read"); ADD_PROPERTY(PropertyInfo(Variant::INT, "stream_bytes_read", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_stream_bytes_read"); ADD_PROPERTY(PropertyInfo(Variant::INT, "other_bytes_read", PropertyHint::PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY), "", "get_other_bytes_read"); } float FmodPerformanceData::get_dsp() const { return dsp; } float FmodPerformanceData::get_geometry() const { return geometry; } float FmodPerformanceData::get_stream() const { return stream; } float FmodPerformanceData::get_update() const { return update; } float FmodPerformanceData::get_convolution1() const { return convolution1; } float FmodPerformanceData::get_convolution2() const { return convolution2; } float FmodPerformanceData::get_studio() const { return studio; } int FmodPerformanceData::get_currently_allocated() const { return currently_allocated; } int FmodPerformanceData::get_max_allocated() const { return max_allocated; } int FmodPerformanceData::get_sample_bytes_read() const { return sample_bytes_read; } int FmodPerformanceData::get_stream_bytes_read() const { return stream_bytes_read; } int FmodPerformanceData::get_other_bytes_read() const { return other_bytes_read; } ================================================ FILE: src/data/performance_data.h ================================================ #ifndef GODOTFMOD_PERFORMANCE_DATA_H #define GODOTFMOD_PERFORMANCE_DATA_H #include "classes/ref_counted.hpp" namespace godot { class FmodPerformanceData : public RefCounted { GDCLASS(FmodPerformanceData, RefCounted); public: float dsp = 0; float geometry = 0; float stream = 0; float update = 0; float convolution1 = 0; float convolution2 = 0; float studio = 0; int currently_allocated = 0; int max_allocated = 0; int sample_bytes_read = 0; int stream_bytes_read = 0; int other_bytes_read = 0; float get_geometry() const; float get_stream() const; float get_update() const; float get_convolution1() const; float get_convolution2() const; float get_studio() const; float get_dsp() const; int get_currently_allocated() const; int get_max_allocated() const; int get_sample_bytes_read() const; int get_stream_bytes_read() const; int get_other_bytes_read() const; protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_PERFORMANCE_DATA_H ================================================ FILE: src/fmod_cache.cpp ================================================ #include "fmod_cache.h" #include "helpers/common.h" #include "classes/project_settings.hpp" using namespace godot; FmodCache::FmodCache(FMOD::Studio::System* p_system, FMOD::System* p_core_system) : system(p_system), core_system(p_core_system) { } FmodCache::~FmodCache() { system = nullptr; core_system = nullptr; } void FmodCache::update_pending() { if (loading_banks.size() == 0) { return; } List> to_delete; for (const Ref& loading_bank : loading_banks) { int loading_state = loading_bank->get_loading_state(); if (loading_state == FMOD_STUDIO_LOADING_STATE_LOADED) { _get_bank_data(loading_bank); banks[loading_bank->get_godot_res_path()] = loading_bank.ptr(); to_delete.push_back(loading_bank); } else if (loading_state == FMOD_STUDIO_LOADING_STATE_ERROR) { to_delete.push_back(loading_bank); GODOT_LOG_ERROR("Fmod Sound System: Error loading bank.") } } for (const Ref& element : to_delete) { loading_banks.erase(element); } } void FmodCache::force_loading() { while (is_loading()) { update_pending(); } } bool FmodCache::is_loading() { return loading_banks.size() > 0; } Ref FmodCache::add_bank(const String& bank_path, unsigned int flag) { FMOD::Studio::Bank* bank = nullptr; ERROR_CHECK_WITH_REASON(system->loadBankFile(bank_path.utf8().get_data(), flag, &bank), vformat("Cannot load bank %s", bank_path)); if (!bank) { return {}; } Ref ref = FmodBank::create_ref(bank, bank_path); GODOT_LOG_VERBOSE("FMOD Sound System: LOADING BANK " + String(bank_path)) loading_banks.push_back(ref); if (flag != FMOD_STUDIO_LOAD_BANK_NONBLOCKING) { force_loading(); } return ref; } void FmodCache::remove_bank(const String& bank_path) { if (!banks.has(bank_path)) { GODOT_LOG_ERROR(vformat("Cannot unload bank with path %s, not in cache.", bank_path)); return; } FmodBank* bank = banks[bank_path]; _remove_bank_data(bank); ERROR_CHECK_WITH_REASON(bank->get_wrapped()->unload(), vformat("Cannot unload bank %s", bank_path)); banks.erase(bank_path); } bool FmodCache::has_bank(const String& bankPath) { return banks.has(bankPath); } Ref FmodCache::get_bank(const String& bankPath) { return banks.get(bankPath); } #ifndef IOS_ENABLED uint32_t FmodCache::add_plugin(const String& p_plugin_path, uint32_t p_priority) { uint32_t handle; #if defined(ANDROID_ENABLED) && !defined(TOOLS_ENABLED) const char* plugin_path = p_plugin_path.utf8().get_data(); #else const char* plugin_path = ProjectSettings::get_singleton()->globalize_path(p_plugin_path).utf8().get_data(); #endif ERROR_CHECK(core_system->loadPlugin(plugin_path, &handle, p_priority)); plugin_handles.append(handle); return handle; } #else void FmodCache::add_plugin(uint32_t p_plugin_handle) { plugin_handles.append(p_plugin_handle); } #endif bool FmodCache::has_plugin(uint32_t p_plugin_handle) const { return plugin_handles.has(p_plugin_handle); } void FmodCache::remove_plugin(uint32_t p_plugin_handle) { if (!has_plugin(p_plugin_handle)) { GODOT_LOG_ERROR(vformat("Cannot unload plugin with handle %s, not in cache.", p_plugin_handle)); return; } ERROR_CHECK(core_system->unloadPlugin(p_plugin_handle)); plugin_handles.erase(p_plugin_handle); } Ref FmodCache::add_file(const String& file_path, unsigned int flag) { FMOD::System* core = nullptr; ERROR_CHECK(system->getCoreSystem(&core)); FMOD::Sound* sound = nullptr; ERROR_CHECK_WITH_REASON(core->createSound(file_path.utf8().get_data(), flag, nullptr, &sound), vformat("Cannot create sound %s", file_path)); if (sound) { Ref ref = FmodFile::create_ref(sound); files[file_path] = ref; GODOT_LOG_VERBOSE("FMOD Sound System: LOADING AS SOUND FILE" + String(file_path)) return ref; } return {}; } bool FmodCache::has_file(const String& filePath) { return files.find(filePath).operator bool(); } Ref FmodCache::get_file(const String& filePath) { return files.get(filePath); } void FmodCache::remove_file(const String& filePath) { if (files.has(filePath)) { Ref ref = files[filePath]; ERROR_CHECK(ref->get_wrapped()->release()); files.erase(filePath); } } bool FmodCache::has_vca_guid(const FMOD_GUID& guid) { return vcas.has(guid); } bool FmodCache::has_vca_path(const String& vcaPath) { return strings_to_guid.has(vcaPath) && vcas.has(strings_to_guid.get(vcaPath)); } bool FmodCache::has_bus_guid(const FMOD_GUID& guid) { return buses.has(guid); } bool FmodCache::has_bus_path(const String& busPath) { return strings_to_guid.has(busPath) && buses.has(strings_to_guid.get(busPath)); } bool FmodCache::has_event_guid(const FMOD_GUID& guid) { return event_descriptions.has(guid); } bool FmodCache::has_event_path(const String& eventPath) { return strings_to_guid.has(eventPath) && event_descriptions.has(strings_to_guid.get(eventPath)); } Ref FmodCache::get_vca(const FMOD_GUID& guid) { if ( HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer>::Iterator iterator{ vcas.find(guid) } ) { return iterator->value; } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find vca with guid: %s", fmod_guid_to_string(guid))); #endif return {}; } Ref FmodCache::get_vca(const String& vca_path) { if (HashMap::Iterator iterator {strings_to_guid.find(vca_path)}) { return get_vca(iterator->value); } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find vca with path: %s", vca_path)); #endif return {}; } Ref FmodCache::get_bus(const FMOD_GUID& guid) { if ( HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer>::Iterator iterator{ buses.find(guid) } ) { return iterator->value; } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find bus with guid: %s", fmod_guid_to_string(guid))); #endif return {}; } Ref FmodCache::get_bus(const String& bus_path) { if (HashMap::Iterator iterator {strings_to_guid.find(bus_path)}) { return get_bus(iterator->value); } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find bus with path: %s", bus_path)); #endif return {}; } Ref FmodCache::get_event(const FMOD_GUID& guid) { if ( HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer>::Iterator iterator { event_descriptions.find(guid) } ) { return iterator->value; } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find event with guid: %s", fmod_guid_to_string(guid))); #endif return {}; } Ref FmodCache::get_event(const String& eventPath) { if (HashMap::Iterator iterator {strings_to_guid.find(eventPath)}) { return get_event(iterator->value); } #ifdef DEBUG_ENABLED GODOT_LOG_WARNING(vformat("Cannot find event with path: %s", eventPath)); #endif return {}; } FMOD_GUID FmodCache::get_event_guid(const String& event_path) { if (HashMap::Iterator iterator {strings_to_guid.find(event_path)}) { return iterator->value; } return {}; } String FmodCache::get_event_path(const FMOD_GUID& guid) { if ( HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer>::Iterator iterator{ event_descriptions.find(guid) } ) { return iterator->value->get_path(); } return {}; } bool FmodCache::is_master_loaded() { return banks.size() > 0; } void FmodCache::clear() { force_loading(); event_descriptions.clear(); buses.clear(); vcas.clear(); banks.clear(); } void FmodCache::_get_bank_data(Ref bank) { bank->update_bank_data(); for (Ref bus : bank->get_buses()) { FMOD_GUID guid {bus->get_guid()}; buses[guid] = bus; strings_to_guid[bus->get_path()] = guid; } for (Ref vca : bank->get_vcas()) { FMOD_GUID guid {vca->get_guid()}; vcas[guid] = vca; strings_to_guid[vca->get_path()] = guid; } for (Ref desc : bank->get_event_descriptions()) { FMOD_GUID guid {desc->get_guid()}; event_descriptions[guid] = desc; strings_to_guid[desc->get_path()] = guid; } } void FmodCache::_remove_bank_data(FmodBank* bank) { for (Ref bus : bank->get_buses()) { strings_to_guid.erase(bus->get_path()); buses.erase(bus->get_guid()); } for (Ref vca : bank->get_vcas()) { strings_to_guid.erase(vca->get_path()); vcas.erase(vca->get_guid()); } for (Ref desc : bank->get_event_descriptions()) { strings_to_guid.erase(desc->get_path()); event_descriptions.erase(desc->get_guid()); } } ================================================ FILE: src/fmod_cache.h ================================================ #ifndef GODOTFMOD_FMOD_CACHE_H #define GODOTFMOD_FMOD_CACHE_H #include "core/fmod_file.h" #include "fmod_studio.hpp" #include "studio/fmod_bank.h" #include "studio/fmod_bus.h" #include "studio/fmod_event_description.h" #include "studio/fmod_vca.h" #include "templates/hash_map.hpp" namespace godot { class FmodServer; struct FmodGuidHashMapHasher { static _FORCE_INLINE_ uint32_t hash(const FMOD_GUID& guid) { return guid.Data1; } }; struct FmodGuidHashMapComparer { static bool compare(const FMOD_GUID& p_lhs, const FMOD_GUID& p_rhs) { uint64_t* left_guid {reinterpret_cast(const_cast(&p_lhs))}; uint64_t* right_guid {reinterpret_cast(const_cast(&p_rhs))}; return left_guid[0] == right_guid[0] && left_guid[1] == right_guid[1]; } }; class FmodCache { friend class FmodServer; FMOD::Studio::System* system; FMOD::System* core_system; List> loading_banks; HashMap> files; HashMap banks; Vector plugin_handles; HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer> event_descriptions; HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer> buses; HashMap, FmodGuidHashMapHasher, FmodGuidHashMapComparer> vcas; HashMap strings_to_guid; void _get_bank_data(Ref bank); void _remove_bank_data(FmodBank* bank); public: FmodCache() = delete; FmodCache(const FmodCache& other) = delete; FmodCache(FMOD::Studio::System* p_system, FMOD::System* p_core_system); ~FmodCache(); Ref add_bank(const String& bank_path, unsigned int flag); bool has_bank(const String& bankPath); Ref get_bank(const String& bankPath); void remove_bank(const String& bank_path); #ifndef IOS_ENABLED uint32_t add_plugin(const String& p_plugin_path, uint32_t p_priority = 0); #else void add_plugin(uint32_t p_plugin_handle); #endif bool has_plugin(uint32_t p_plugin_handle) const; void remove_plugin(uint32_t p_plugin_handle); Ref add_file(const String& filePath, unsigned int flag); bool has_file(const String& filePath); Ref get_file(const String& filePath); void remove_file(const String& filePath); bool is_master_loaded(); void clear(); void update_pending(); void force_loading(); bool is_loading(); bool has_vca_guid(const FMOD_GUID& guid); bool has_vca_path(const String& vcaPath); bool has_bus_guid(const FMOD_GUID& guid); bool has_bus_path(const String& busPath); bool has_event_guid(const FMOD_GUID& guid); bool has_event_path(const String& eventPath); Ref get_vca(const FMOD_GUID& guid); Ref get_vca(const String& vca_path); Ref get_bus(const FMOD_GUID& guid); Ref get_bus(const String& bus_path); Ref get_event(const FMOD_GUID& guid); Ref get_event(const String& eventPath); FMOD_GUID get_event_guid(const String& event_path); String get_event_path(const FMOD_GUID& guid); }; }// namespace godot #endif// GODOTFMOD_FMOD_CACHE_H ================================================ FILE: src/fmod_logging.cpp ================================================ #include "fmod_logging.h" #include #include #include #include #include #include namespace godot { // initialize FMOD logging based on project settings void logging_init() { const Ref p_logging_settings = FmodLoggingSettings::get_from_project_settings(); if (p_logging_settings.is_valid()) { unsigned int debug_flags = p_logging_settings->_debug_level_to_fmod(); FMOD_DEBUG_MODE log_output = static_cast(p_logging_settings->get_log_output()); switch (log_output) { case FMOD_DEBUG_MODE_TTY: { // Output to terminal/console FMOD::Debug_Initialize(debug_flags, FMOD_DEBUG_MODE_TTY, nullptr, nullptr); break; } case FMOD_DEBUG_MODE_CALLBACK: { // Output to a callback -> GODOT FMOD::Debug_Initialize(debug_flags, FMOD_DEBUG_MODE_CALLBACK, fmod_debug_callback, nullptr); break; } case FMOD_DEBUG_MODE_FILE: { UtilityFunctions::push_warning("FMOD log output set to File"); // Output to a file String file_path = p_logging_settings->get_log_file_path(); CharString file_path_utf8 = file_path.utf8(); file_path = ProjectSettings::get_singleton()->globalize_path(file_path); file_path_utf8 = file_path.utf8(); FMOD::Debug_Initialize(debug_flags, FMOD_DEBUG_MODE_FILE, nullptr, file_path_utf8); break; } default: { // Fallback to TTY if somehow an invalid value is set FMOD::Debug_Initialize(debug_flags, FMOD_DEBUG_MODE_TTY, nullptr, nullptr); UtilityFunctions::push_warning("Invalid FMOD log output setting, defaulting to TTY"); break; } } } } // Direct logging function implementation void log_fmod_message(FMODLogLevel level, const String& message) { switch (level) { case LOG_ERROR: UtilityFunctions::push_error(message); break; case LOG_WARNING: UtilityFunctions::push_warning(message); break; case LOG_VERBOSE: UtilityFunctions::print_verbose(message); break; case LOG_INFO: default: UtilityFunctions::print(message); break; } } extern "C" { FMOD_RESULT fmod_debug_callback(FMOD_DEBUG_FLAGS flags, const char* file, int line, const char* func, const char* message) { if (!message) { return FMOD_OK; } String debug_message; // Determine the log level and prefix FMODLogLevel log_level = LOG_INFO; if (flags & FMOD_DEBUG_LEVEL_ERROR) { debug_message += "[FMOD ERROR]"; log_level = LOG_ERROR; } else if (flags & FMOD_DEBUG_LEVEL_WARNING) { debug_message += "[FMOD WARN]"; log_level = LOG_WARNING; } else if (flags & FMOD_DEBUG_LEVEL_LOG) { debug_message += "[FMOD INFO]"; } else { debug_message += "[FMOD]"; } // Add type information if available if (flags & FMOD_DEBUG_TYPE_MEMORY) { debug_message += "[MEM]"; } if (flags & FMOD_DEBUG_TYPE_FILE) { debug_message += "[FILE]"; } if (flags & FMOD_DEBUG_TYPE_CODEC) { debug_message += "[CODEC]"; } if (flags & FMOD_DEBUG_TYPE_TRACE) { debug_message += "[TRACE]"; } // Format the message based on display flags if ((flags & FMOD_DEBUG_DISPLAY_LINENUMBERS) && file && func) { debug_message += String(" ") + file + ":" + String::num_int64(line) + " in " + func + "(): " + message; } else if (file && func) { debug_message += String(" ") + file + " in " + func + "(): " + message; } else if (func) { debug_message += String(" ") + func + "(): " + message; } else { debug_message += String(" ") + message; } debug_message.strip_edges(); log_fmod_message(log_level, debug_message); return FMOD_OK; } } }// namespace godot ================================================ FILE: src/fmod_logging.h ================================================ #ifndef GODOTFMOD_FMOD_LOGGING_H #define GODOTFMOD_FMOD_LOGGING_H #include "fmod_common.h" #include #include namespace godot { // Log level enumeration enum FMODLogLevel { LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_VERBOSE// Added for verbose logging }; void logging_init(); // Core logging function void log_fmod_message(FMODLogLevel level, const String& message); // FMOD debug callback function extern "C" { FMOD_RESULT fmod_debug_callback(FMOD_DEBUG_FLAGS flags, const char* file, int line, const char* func, const char* message); } }// namespace godot #endif ================================================ FILE: src/fmod_server.cpp ================================================ #include "classes/dir_access.hpp" #include "classes/engine.hpp" #include "classes/os.hpp" #include "core/fmod_sound.h" #include "data/performance_data.h" #include "fmod_logging.h" #include "helpers/common.h" #include "helpers/maths.h" #include "plugins/ios_plugins_loader.h" #include "plugins/plugins_helper.h" #include #include #include using namespace godot; FmodServer* FmodServer::singleton = nullptr; void FmodServer::_bind_methods() { // LIFECYCLE ClassDB::bind_method(D_METHOD("init", "p_settings"), &FmodServer::init); ClassDB::bind_method(D_METHOD("update"), &FmodServer::update); ClassDB::bind_method(D_METHOD("shutdown"), &FmodServer::shutdown); // SETTINGS ClassDB::bind_method(D_METHOD("set_software_format", "p_settings"), &FmodServer::set_software_format); ClassDB::bind_method(D_METHOD("set_sound_3D_settings", "p_settings"), &FmodServer::set_sound_3d_settings); ClassDB::bind_method(D_METHOD("set_system_dsp_buffer_size", "dsp_settings"), &FmodServer::set_system_dsp_buffer_size); ClassDB::bind_method(D_METHOD("get_system_dsp_buffer_settings"), &FmodServer::get_system_dsp_buffer_settings); ClassDB::bind_method(D_METHOD("get_system_dsp_buffer_length"), &FmodServer::get_system_dsp_buffer_length); ClassDB::bind_method(D_METHOD("get_system_dsp_num_buffers"), &FmodServer::get_system_dsp_num_buffers); // OBJECT ClassDB::bind_method(D_METHOD("check_vca_guid", "guid"), &FmodServer::check_vca_guid); ClassDB::bind_method(D_METHOD("check_vca_path", "cvaPath"), &FmodServer::check_vca_path); ClassDB::bind_method(D_METHOD("check_bus_guid", "guid"), &FmodServer::check_bus_guid); ClassDB::bind_method(D_METHOD("check_bus_path", "busPath"), &FmodServer::check_bus_path); ClassDB::bind_method(D_METHOD("check_event_guid", "guid"), &FmodServer::check_event_guid); ClassDB::bind_method(D_METHOD("check_event_path", "eventPath"), &FmodServer::check_event_path); ClassDB::bind_method(D_METHOD("get_vca_from_guid", "guid"), &FmodServer::get_vca_from_guid); ClassDB::bind_method(D_METHOD("get_vca", "cvaPath"), &FmodServer::get_vca); ClassDB::bind_method(D_METHOD("get_bus_from_guid", "guid"), &FmodServer::get_bus_from_guid); ClassDB::bind_method(D_METHOD("get_bus", "busPath"), &FmodServer::get_bus); ClassDB::bind_method(D_METHOD("get_event_from_guid", "guid"), &FmodServer::get_event_from_guid); ClassDB::bind_method(D_METHOD("get_event", "eventPath"), &FmodServer::get_event); ClassDB::bind_method(D_METHOD("get_event_guid", "event_path"), &FmodServer::get_event_guid); ClassDB::bind_method(D_METHOD("get_event_path", "guid"), &FmodServer::get_event_path); ClassDB::bind_method(D_METHOD("get_all_vca"), &FmodServer::get_all_vca); ClassDB::bind_method(D_METHOD("get_all_buses"), &FmodServer::get_all_buses); ClassDB::bind_method(D_METHOD("get_all_event_descriptions"), &FmodServer::get_all_event_descriptions); ClassDB::bind_method(D_METHOD("get_all_banks"), &FmodServer::get_all_banks); // DEBUGGING ClassDB::bind_method(D_METHOD("get_available_drivers"), &FmodServer::get_available_drivers); ClassDB::bind_method(D_METHOD("get_driver"), &FmodServer::get_driver); ClassDB::bind_method(D_METHOD("set_driver", "id"), &FmodServer::set_driver); ClassDB::bind_method(D_METHOD("get_performance_data"), &FmodServer::get_performance_data); // GLOBAL PARAMETERS ClassDB::bind_method(D_METHOD("set_global_parameter_by_name", "parameter_name", "value"), &FmodServer::set_global_parameter_by_name); ClassDB::bind_method(D_METHOD("set_global_parameter_by_name_with_label", "parameter_name", "label"), &FmodServer::set_global_parameter_by_name_with_label); ClassDB::bind_method(D_METHOD("get_global_parameter_by_name", "parameter_name"), &FmodServer::get_global_parameter_by_name); ClassDB::bind_method(D_METHOD("set_global_parameter_by_id", "parameter_id", "value"), &FmodServer::set_global_parameter_by_id); ClassDB::bind_method(D_METHOD("set_global_parameter_by_id_with_label", "parameter_id", "label"), &FmodServer::set_global_parameter_by_id_with_label); ClassDB::bind_method(D_METHOD("get_global_parameter_by_id", "parameter_id"), &FmodServer::get_global_parameter_by_id); ClassDB::bind_method(D_METHOD("get_global_parameter_desc_by_name", "parameterName"), &FmodServer::get_global_parameter_desc_by_name); ClassDB::bind_method(D_METHOD("get_global_parameter_desc_by_id", "parameter_id"), &FmodServer::get_global_parameter_desc_by_id); ClassDB::bind_method(D_METHOD("get_global_parameter_desc_count"), &FmodServer::get_global_parameter_desc_count); ClassDB::bind_method(D_METHOD("get_global_parameter_desc_list"), &FmodServer::get_global_parameter_desc_list); // LISTENERS ClassDB::bind_method(D_METHOD("add_listener", "index", "game_obj"), &FmodServer::add_listener); ClassDB::bind_method(D_METHOD("remove_listener", "index", "game_obj"), &FmodServer::remove_listener); ClassDB::bind_method(D_METHOD("set_listener_number", "listenerNumber"), &FmodServer::set_system_listener_number); ClassDB::bind_method(D_METHOD("get_listener_number"), &FmodServer::get_system_listener_number); ClassDB::bind_method(D_METHOD("get_listener_weight", "index"), &FmodServer::get_system_listener_weight); ClassDB::bind_method(D_METHOD("set_listener_weight", "index", "weight"), &FmodServer::set_system_listener_weight); ClassDB::bind_method(D_METHOD("get_listener_transform3d", "index"), &FmodServer::get_listener_transform3d); ClassDB::bind_method(D_METHOD("get_listener_transform2d", "index"), &FmodServer::get_listener_transform2d); ClassDB::bind_method(D_METHOD("get_listener_3d_velocity", "index"), &FmodServer::get_listener_3d_velocity); ClassDB::bind_method(D_METHOD("get_listener_2d_velocity", "index"), &FmodServer::get_listener_2d_velocity); ClassDB::bind_method(D_METHOD("set_listener_transform3d", "index", "transform"), &FmodServer::set_listener_transform3d); ClassDB::bind_method(D_METHOD("set_listener_transform2d", "index", "transform"), &FmodServer::set_listener_transform2d); ClassDB::bind_method(D_METHOD("set_listener_lock", "index", "isLocked"), &FmodServer::set_listener_lock); ClassDB::bind_method(D_METHOD("get_listener_lock", "index"), &FmodServer::get_listener_lock); ClassDB::bind_method(D_METHOD("get_object_attached_to_listener", "index"), &FmodServer::get_object_attached_to_listener); // BANKS ClassDB::bind_method(D_METHOD("load_bank", "pathToBank", "flag"), &FmodServer::load_bank); ClassDB::bind_method(D_METHOD("wait_for_all_loads"), &FmodServer::wait_for_all_loads); ClassDB::bind_method(D_METHOD("banks_still_loading"), &FmodServer::banks_still_loading); // PLUGINS ClassDB::bind_method(D_METHOD("load_plugin", "p_plugin_path", "p_priority"), &FmodServer::load_plugin, DEFVAL(0)); ClassDB::bind_method(D_METHOD("unload_plugin", "p_plugin_handle"), &FmodServer::unload_plugin); ClassDB::bind_method(D_METHOD("is_plugin_loaded", "p_plugin_handle"), &FmodServer::is_plugin_loaded); ClassDB::bind_method(D_METHOD("load_file_as_sound", "path"), &FmodServer::load_file_as_sound); ClassDB::bind_method(D_METHOD("load_file_as_music", "path"), &FmodServer::load_file_as_music); ClassDB::bind_method(D_METHOD("unload_file", "path"), &FmodServer::unload_file); ClassDB::bind_method(D_METHOD("create_event_instance_with_guid", "guid"), &FmodServer::create_event_instance_with_guid); ClassDB::bind_method(D_METHOD("create_event_instance", "eventPath"), &FmodServer::create_event_instance); ClassDB::bind_method(D_METHOD("create_event_instance_from_description", "eventPath"), &FmodServer::create_event_instance_from_description); ClassDB::bind_method(D_METHOD("play_one_shot_using_guid", "guid"), &FmodServer::play_one_shot_using_guid); ClassDB::bind_method(D_METHOD("play_one_shot", "event_name"), &FmodServer::play_one_shot); ClassDB::bind_method(D_METHOD("play_one_shot_using_event_description", "event_description"), &FmodServer::play_one_shot_using_event_description); ClassDB::bind_method(D_METHOD("play_one_shot_using_guid_with_params", "guid", "parameters"), &FmodServer::play_one_shot_using_guid_with_params); ClassDB::bind_method(D_METHOD("play_one_shot_with_params", "event_name", "parameters"), &FmodServer::play_one_shot_with_params); ClassDB::bind_method( D_METHOD("play_one_shot_using_event_description_with_params", "event_description", "parameters"), &FmodServer::play_one_shot_using_event_description_with_params ); ClassDB::bind_method(D_METHOD("play_one_shot_using_guid_attached", "guid", "game_obj"), &FmodServer::play_one_shot_using_guid_attached); ClassDB::bind_method(D_METHOD("play_one_shot_attached", "event_name", "game_obj"), &FmodServer::play_one_shot_attached); ClassDB::bind_method( D_METHOD("play_one_shot_using_event_description_attached", "event_description", "game_obj"), &FmodServer::play_one_shot_using_event_description_attached ); ClassDB::bind_method( D_METHOD("play_one_shot_using_guid_attached_with_params", "guid", "game_obj", "parameters"), &FmodServer::play_one_shot_using_guid_attached_with_params ); ClassDB::bind_method(D_METHOD("play_one_shot_attached_with_params", "event_name", "game_obj", "parameters"), &FmodServer::play_one_shot_attached_with_params); ClassDB::bind_method( D_METHOD("play_one_shot_using_event_description_attached_with_params", "event_description", "game_obj", "parameters"), &FmodServer::play_one_shot_using_event_description_attached_with_params ); ClassDB::bind_method(D_METHOD("pause_all_events"), &FmodServer::pause_all_events); ClassDB::bind_method(D_METHOD("unpause_all_events"), &FmodServer::unpause_all_events); ClassDB::bind_method(D_METHOD("mute_all_events"), &FmodServer::mute_all_events); ClassDB::bind_method(D_METHOD("unmute_all_events"), &FmodServer::unmute_all_events); ClassDB::bind_method(D_METHOD("mixer_suspend"), &FmodServer::mixer_suspend); ClassDB::bind_method(D_METHOD("mixer_resume"), &FmodServer::mixer_resume); ClassDB::bind_method(D_METHOD("create_sound_instance", "path"), &FmodServer::create_sound_instance); REGISTER_ALL_CONSTANTS } FmodServer::FmodServer() : system(nullptr), coreSystem(nullptr), isInitialized(false), isNotInitializedPrinted(false), distanceScale(1.0), cache(nullptr) { ERR_FAIL_COND(singleton != nullptr); singleton = this; callback_mutex.instantiate(); performanceData = create_ref(); Callbacks::GodotFileRunner::get_singleton()->start(); } FmodServer::~FmodServer() { callbacks_to_process.clear(); ERR_FAIL_COND(singleton != this); singleton = nullptr; } FmodServer* FmodServer::get_singleton() { return singleton; } void FmodServer::init(const Ref& p_settings) { if (isInitialized) { GODOT_LOG_WARNING("Fmod system already initialized.") return; } logging_init(); // initialize FMOD Studio and FMOD Core System with provided flags if (system == nullptr && coreSystem == nullptr) { ERROR_CHECK(FMOD::Studio::System::create(&system)); ERROR_CHECK(system->getCoreSystem(&coreSystem)); } // editing advanced settings to set random seed before system initialization FMOD_ADVANCEDSETTINGS advancedSettings = {}; advancedSettings.cbSize = sizeof(FMOD_ADVANCEDSETTINGS); ERROR_CHECK(coreSystem->getAdvancedSettings(&advancedSettings)); advancedSettings.randomSeed = static_cast(std::time(nullptr));// Use time as a seed ERROR_CHECK(coreSystem->setAdvancedSettings(&advancedSettings)); FMOD_STUDIO_INITFLAGS studio_init_flags = FMOD_STUDIO_INIT_NORMAL; if ( #ifdef TOOLS_ENABLED !Engine::get_singleton()->is_editor_hint() && #endif p_settings->get_is_live_update_enabled() ) { studio_init_flags |= FMOD_STUDIO_INIT_LIVEUPDATE; } if (p_settings->get_is_memory_tracking_enabled()) { studio_init_flags |= FMOD_STUDIO_INIT_MEMORY_TRACKING; } FMOD_INITFLAGS init_flags = FMOD_INIT_3D_RIGHTHANDED; if (ERROR_CHECK(system->initialize(p_settings->get_channel_count(), studio_init_flags, init_flags, nullptr))) { isInitialized = true; GODOT_LOG_INFO("FMOD Sound System: Successfully initialized") if ((studio_init_flags & FMOD_STUDIO_INIT_LIVEUPDATE) == FMOD_STUDIO_INIT_LIVEUPDATE) { GODOT_LOG_INFO("FMOD Sound System: Live update enabled!") } if ((studio_init_flags & FMOD_STUDIO_INIT_MEMORY_TRACKING) == FMOD_STUDIO_INIT_MEMORY_TRACKING) { GODOT_LOG_INFO("FMOD Sound System: Memory tracking enabled!") } } if (ERROR_CHECK( coreSystem->setFileSystem(&Callbacks::godotFileOpen, &Callbacks::godotFileClose, nullptr, nullptr, &Callbacks::godotSyncRead, &Callbacks::godotSyncCancel, -1) )) { GODOT_LOG_VERBOSE("Custom File System enabled.") } cache = new FmodCache(system, coreSystem); } void FmodServer::update() { if (!isInitialized) { if (!isNotInitializedPrinted) { GODOT_LOG_ERROR("FMOD Sound System: Fmod should be initialized before calling update") isNotInitializedPrinted = true; } return; } // Check if bank are loaded, load buses, vca and event descriptions. cache->update_pending(); callback_mutex->lock(); for (const Callback& callback : callbacks_to_process) { if (!callback.callable.is_valid()) { continue; }// Don't run the callback if the object has been killed godot::Array args = godot::Array(); args.append(callback.fmod_callback_properties); args.append(callback.type); callback.callable.callv(args); } callbacks_to_process.clear(); callback_mutex->unlock(); Vector one_shots_copy = oneShots; for (OneShot* oneShot : one_shots_copy) { if (!oneShot->instance->is_valid() || !oneShot->wrapper.is_valid()) { // We free oneShot when their event or Object is dead. oneShots.erase(oneShot); delete oneShot; continue; } oneShot->instance->set_node_attributes(oneShot->wrapper.get_node()); } Vector> events_copy = runningEvents; for (const Ref& event : events_copy) { if (!event->is_valid()) { runningEvents.erase(event); } } #ifdef TOOLS_ENABLED if (!Engine::get_singleton()->is_editor_hint()) { #endif // Editor only needs to run the server for events preview in the explorer. // We don't need to update performance_data and listeners _set_listener_attributes(); _update_performance_data(); #ifdef TOOLS_ENABLED } #endif ERROR_CHECK(system->update()); } void FmodServer::_set_listener_attributes() { for (int i = 0; i < systemListenerNumber; ++i) { Listener* listener = &listeners[i]; if (listener->listenerLock) { continue; } if (!listener->wrapper.is_valid()) { listener->wrapper.set_node(nullptr); ERROR_CHECK_WITH_REASON(system->setListenerWeight(i, 0), vformat("Cannot set listener %d weight to 0", i)); continue; } Node* node {listener->wrapper.get_node()}; if (!node->is_inside_tree()) { return; } if (auto* ci {Node::cast_to(node)}) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform2d(ci->get_global_transform(), distanceScale); ERROR_CHECK_WITH_REASON(system->setListenerAttributes(i, &attr), vformat("Cannot set listener %d attributes", i)); continue; } if (auto* s {Node::cast_to(node)}) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform3d(s->get_global_transform(), distanceScale); ERROR_CHECK_WITH_REASON(system->setListenerAttributes(i, &attr), vformat("Cannot set listener %d attributes", i)); continue; } } } void FmodServer::shutdown() { if (!isInitialized) { return; } FMOD::Debug_Initialize(FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING, FMOD_DEBUG_MODE_TTY, nullptr, nullptr); isInitialized = false; isNotInitializedPrinted = false; ERROR_CHECK(system->unloadAll()); ERROR_CHECK(system->release()); system = nullptr; coreSystem = nullptr; delete cache; cache = nullptr; GODOT_LOG_INFO("FMOD Sound System: System released") } void FmodServer::set_system_listener_number(int p_listenerNumber) { if (p_listenerNumber > 0 && p_listenerNumber <= FMOD_MAX_LISTENERS) { if (ERROR_CHECK_WITH_REASON(system->setNumListeners(p_listenerNumber), vformat("Cannot set listener count to %d", p_listenerNumber))) { systemListenerNumber = p_listenerNumber; } } else { GODOT_LOG_ERROR("Number of listeners must be set between 1 and 8") } } void FmodServer::add_listener(int index, Node* game_obj) { if (index >= 0 && index < systemListenerNumber) { Listener* listener = &listeners[index]; listener->wrapper.set_node(game_obj); ERROR_CHECK_WITH_REASON( system->setListenerWeight(index, listener->weight), vformat("Cannot set listener %d weight to %f", index, listener->weight) ); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } void FmodServer::remove_listener(int index, Node* game_obj) { if (index >= 0 && index < systemListenerNumber) { Listener* listener = &listeners[index]; if (listener->wrapper.get_node() != game_obj) { return; } listener->wrapper.set_node(nullptr); ERROR_CHECK_WITH_REASON(system->setListenerWeight(index, 0), vformat("Cannot set listener %d weight to 0", index)); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } int FmodServer::get_system_listener_number() const { return systemListenerNumber; } float FmodServer::get_system_listener_weight(const int index) { if (index >= 0 && index < systemListenerNumber) { float weight = 0; ERROR_CHECK_WITH_REASON(system->getListenerWeight(index, &weight), vformat("Cannot get listener %d weight", index)); listeners[index].weight = weight; return weight; } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") return 0; } } void FmodServer::set_system_listener_weight(const int index, float weight) { if (index >= 0 && index < systemListenerNumber) { listeners[index].weight = weight; ERROR_CHECK_WITH_REASON(system->setListenerWeight(index, weight), vformat("Cannot set listener %d weight to %f", index, weight)); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } Transform3D FmodServer::get_listener_transform3d(int index) { Transform3D transform; if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr; ERROR_CHECK_WITH_REASON(system->getListenerAttributes(index, &attr), vformat("Cannot get listener %d transform3d", index)); transform = get_transform3d_from_3d_attributes(attr, distanceScale); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } return transform; } Transform2D FmodServer::get_listener_transform2d(int index) { Transform2D transform; if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr; ERROR_CHECK_WITH_REASON(system->getListenerAttributes(index, &attr), vformat("Cannot get listener %d transform2d", index)); transform = get_transform2d_from_3d_attributes(attr, distanceScale); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } return transform; } Vector3 FmodServer::get_listener_3d_velocity(int index) { Vector3 velocity; if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr; ERROR_CHECK_WITH_REASON(system->getListenerAttributes(index, &attr), vformat("Cannot get listener %d velocity", index)); velocity = get_velocity3d_from_3d_attributes(attr, distanceScale); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } return velocity; } Vector2 FmodServer::get_listener_2d_velocity(int index) { Vector2 velocity; if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr; ERROR_CHECK_WITH_REASON(system->getListenerAttributes(index, &attr), vformat("Cannot get listener %d velocity", index)); velocity = get_velocity2d_from_3d_attributes(attr, distanceScale); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } return velocity; } void FmodServer::set_listener_transform3d(int index, const Transform3D& transform) { if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform3d(transform, distanceScale); ERROR_CHECK_WITH_REASON(system->setListenerAttributes(index, &attr), vformat("Cannot set listener %d transform3d", index)); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } void FmodServer::set_listener_transform2d(int index, const Transform2D& transform) { if (index >= 0 && index < systemListenerNumber) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform2d(transform, distanceScale); ERROR_CHECK_WITH_REASON(system->setListenerAttributes(index, &attr), vformat("Cannot set listener %d transform2d", index)); } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } void FmodServer::set_listener_lock(int index, bool isLocked) { if (index >= 0 && index < systemListenerNumber) { listeners[index].listenerLock = isLocked; } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") } } bool FmodServer::get_listener_lock(int index) { if (index >= 0 && index < systemListenerNumber) { return listeners[index].listenerLock; } else { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") return false; } } Object* FmodServer::get_object_attached_to_listener(const int index) { if (index < 0 || index >= systemListenerNumber) { GODOT_LOG_ERROR("index of listeners must be set between 0 and the number of listeners set") return nullptr; } Object* node = listeners[index].wrapper.get_node(); return node; } void FmodServer::set_software_format(const Ref& p_settings) { if (system == nullptr && coreSystem == nullptr) { ERROR_CHECK(FMOD::Studio::System::create(&system)); ERROR_CHECK(system->getCoreSystem(&coreSystem)); } ERROR_CHECK(coreSystem->setSoftwareFormat( p_settings->get_sample_rate(), static_cast(p_settings->get_speaker_mode()), p_settings->get_raw_speakers_count() )); } Ref FmodServer::load_bank(const String& pathToBank, unsigned int flag) { if (cache->has_bank(pathToBank)) { return cache->get_bank(pathToBank); }// bank is already loaded #ifdef DEBUG_ENABLED if (!FileAccess::file_exists(pathToBank)) { GODOT_LOG_ERROR(vformat("Cannot load bank at %s", pathToBank)) return {}; } #endif return cache->add_bank(pathToBank, flag); } void FmodServer::unload_bank(const String& pathToBank) { #ifdef DEBUG_ENABLED if (!FileAccess::file_exists(pathToBank)) { GODOT_LOG_ERROR(vformat("Cannot unload bank at %s", pathToBank)) return; } #endif cache->remove_bank(pathToBank); } bool FmodServer::banks_still_loading() { return cache->is_loading(); } #ifdef IOS_ENABLED uint32_t register_ios_dsp(FMOD_SYSTEM_PTR system, FMOD_DSP_DESCRIPTION* description, uint32_t* handle) { return reinterpret_cast(system)->registerDSP(description, handle); } uint32_t register_ios_codec(FMOD_SYSTEM_PTR system, FMOD_CODEC_DESCRIPTION* description, uint32_t* handle) { return reinterpret_cast(system)->registerCodec(description, handle); } uint32_t register_ios_output(FMOD_SYSTEM_PTR system, FMOD_OUTPUT_DESCRIPTION* description, uint32_t* handle) { return reinterpret_cast(system)->registerOutput(description, handle); } #endif void FmodServer::load_all_plugins(const Ref& p_settings) { #ifndef IOS_ENABLED Vector plugin_paths = get_fmod_plugins_libraries_paths(p_settings); for (const String& path : plugin_paths) { #ifdef DEBUG_ENABLED GODOT_LOG_INFO(vformat("Will load %s", path)); #endif load_plugin(path); } #else FMOD_IOS_INTERFACE interface { .system = coreSystem, .register_dsp_method = ®ister_ios_dsp, .register_codec_method = ®ister_ios_codec, .register_output_method = ®ister_ios_output }; uint32_t plugin_count; uint32_t* plugin_handles = load_all_fmod_plugins(&interface, &plugin_count); for (uint32_t i = 0; i < plugin_count; ++i) { cache->add_plugin(plugin_handles[i]); } std::free(plugin_handles); #endif } uint32_t FmodServer::load_plugin(const String& p_plugin_path, uint32_t p_priority) { #ifndef IOS_ENABLED return cache->add_plugin(p_plugin_path, p_priority); #endif return 0xFFFFFFFF; } void FmodServer::unload_plugin(uint32_t p_plugin_handle) { cache->remove_plugin(p_plugin_handle); } bool FmodServer::is_plugin_loaded(uint32_t p_plugin_handle) { return cache->has_plugin(p_plugin_handle); } bool FmodServer::check_vca_guid(const String& guid) { return cache->has_vca_guid(string_to_fmod_guid(guid.utf8().get_data())); } bool FmodServer::check_vca_path(const String& vcaPath) { return cache->has_vca_path(vcaPath); } bool FmodServer::check_bus_guid(const String& guid) { return cache->has_bus_guid(string_to_fmod_guid(guid.utf8().get_data())); } bool FmodServer::check_bus_path(const String& busPath) { return cache->has_bus_path(busPath); } bool FmodServer::check_event_guid(const String& guid) { return cache->has_event_guid(string_to_fmod_guid(guid.utf8().get_data())); } bool FmodServer::check_event_guid_internal(const FMOD_GUID& guid) { return cache->has_event_guid(guid); } bool FmodServer::check_event_path(const String& eventPath) { return cache->has_event_path(eventPath); } Ref FmodServer::get_vca_from_guid(const String& guid) { return cache->get_vca(string_to_fmod_guid(guid.utf8().get_data())); } Ref FmodServer::get_vca(const String& vcaPath) { return cache->get_vca(vcaPath); } Ref FmodServer::get_bus_from_guid(const String& guid) { return cache->get_bus(string_to_fmod_guid(guid.utf8().get_data())); } Ref FmodServer::get_bus(const String& busPath) { return cache->get_bus(busPath); } Ref FmodServer::get_event_from_guid(const String& guid) { return cache->get_event(string_to_fmod_guid(guid.utf8().get_data())); } Ref FmodServer::get_event_from_guid_internal(const FMOD_GUID& guid) { return cache->get_event(guid); } Ref FmodServer::get_event(const String& eventPath) { return cache->get_event(eventPath); } FMOD_GUID FmodServer::get_event_guid_internal(const String& event_path) { return cache->get_event_guid(event_path); } String FmodServer::get_event_guid(const String& event_path) { return fmod_guid_to_string(get_event_guid_internal(event_path)); } String FmodServer::get_event_path_internal(const FMOD_GUID& guid) { return cache->get_event_path(guid); } String FmodServer::get_event_path(const String& guid) { return cache->get_event_path(string_to_fmod_guid(guid.utf8().get_data())); } Array FmodServer::get_all_vca() { Array array; for (KeyValue>& entry : cache->vcas) { array.append(entry.value); } return array; } Array FmodServer::get_all_buses() { Array array; for (KeyValue>& entry : cache->buses) { array.append(entry.value); } return array; } Array FmodServer::get_all_event_descriptions() { Array array; for (KeyValue>& entry : cache->event_descriptions) { array.append(entry.value); } return array; } Array FmodServer::get_all_banks() { Array array; for (KeyValue& entry : cache->banks) { array.append(Ref(entry.value)); } return array; } Ref FmodServer::create_event_instance_with_guid(const String& guid) { return create_event_instance_with_guid_internal(string_to_fmod_guid(guid.utf8().get_data())); } Ref FmodServer::create_event_instance_with_guid_internal(const FMOD_GUID& guid) { EventIdentifier parameters {}; parameters.guid = guid; return _create_event_instance(parameters); } Ref FmodServer::create_event_instance(const String& eventPath) { return _create_event_instance({eventPath.utf8().get_data()}); } Ref FmodServer::create_event_instance_from_description(const Ref& event_description) { EventIdentifier parameter {}; parameter.event_description = event_description.ptr(); return _create_event_instance(parameter); } Ref FmodServer::_get_event_description(const String& event_name) { bool found = cache->has_event_path(event_name); if (!found) { GODOT_LOG_WARNING("Event " + event_name + " can't be found. Check if the path is correct or the bank properly loaded.") return {}; } return cache->get_event(event_name); } Ref FmodServer::_get_event_description(const FMOD_GUID& guid) { bool found = cache->has_event_guid(guid); if (!found) { String fmod_guid_string {fmod_guid_to_string(guid)}; GODOT_LOG_WARNING("Event " + fmod_guid_string + " can't be found. Check if the path is correct or the bank properly loaded.") return {}; } return cache->get_event(guid); } void FmodServer::play_one_shot_using_guid(const String& guid) { EventIdentifier parameter {}; parameter.guid = string_to_fmod_guid(guid.utf8().get_data()); return _play_one_shot(parameter, nullptr); } void FmodServer::play_one_shot(const String& event_name) { return _play_one_shot({event_name.utf8().get_data()}, nullptr); } void FmodServer::play_one_shot_using_event_description(const Ref& event_description) { EventIdentifier parameter {}; parameter.event_description = event_description.ptr(); return _play_one_shot(parameter, nullptr); } void FmodServer::play_one_shot_using_guid_with_params(const String& guid, const Dictionary& parameters) { EventIdentifier parameter {}; parameter.guid = string_to_fmod_guid(guid.utf8().get_data()); return _play_one_shot(parameter, nullptr, parameters); } void FmodServer::play_one_shot_with_params(const String& event_name, const Dictionary& parameters) { return _play_one_shot({event_name.utf8().get_data()}, nullptr, parameters); } void FmodServer::play_one_shot_using_event_description_with_params(const Ref& event_description, const Dictionary& parameters) { EventIdentifier parameter {}; parameter.event_description = event_description.ptr(); return _play_one_shot(parameter, nullptr, parameters); } void FmodServer::play_one_shot_using_guid_attached(const String& guid, Node* game_obj) { EventIdentifier parameter {}; parameter.guid = string_to_fmod_guid(guid.utf8().get_data()); return _play_one_shot(parameter, game_obj); } void FmodServer::play_one_shot_attached(const String& event_name, Node* game_obj) { return _play_one_shot({event_name.utf8().get_data()}, game_obj); } void FmodServer::play_one_shot_using_event_description_attached(const Ref& event_description, Node* game_obj) { EventIdentifier parameter {}; parameter.event_description = event_description.ptr(); return _play_one_shot(parameter, game_obj); } void FmodServer::play_one_shot_using_guid_attached_with_params(const String& guid, Node* game_obj, const Dictionary& parameters) { EventIdentifier parameter {}; parameter.guid = string_to_fmod_guid(guid.utf8().get_data()); return _play_one_shot(parameter, game_obj, parameters); } void FmodServer::play_one_shot_attached_with_params(const String& event_name, Node* game_obj, const Dictionary& parameters) { return _play_one_shot({event_name.utf8().get_data()}, game_obj, parameters); } void FmodServer::play_one_shot_using_event_description_attached_with_params( const Ref& event_description, Node* game_obj, const Dictionary& parameters ) { EventIdentifier parameter {}; parameter.event_description = event_description.ptr(); return _play_one_shot(parameter, game_obj, parameters); } void FmodServer::set_system_dsp_buffer_size(const Ref& p_settings) { unsigned int buffer_length = p_settings->get_dsp_buffer_size(); int num_buffers = p_settings->get_dsp_buffer_count(); if (buffer_length > 0 && num_buffers > 0 && ERROR_CHECK(coreSystem->setDSPBufferSize(buffer_length, num_buffers))) { GODOT_LOG_VERBOSE("FMOD Sound System: Successfully set DSP buffer size") } else { GODOT_LOG_ERROR(vformat("FMOD Sound System: Failed to set DSP buffer size: %s, with buffer count: %s", buffer_length, num_buffers)) } } Ref FmodServer::get_system_dsp_buffer_settings() { unsigned int buffer_length; int num_buffers; Ref ret; ret.instantiate(); ERROR_CHECK(coreSystem->getDSPBufferSize(&buffer_length, &num_buffers)); ret->set_dsp_buffer_size(buffer_length); ret->set_dsp_buffer_count(num_buffers); return ret; } unsigned int FmodServer::get_system_dsp_buffer_length() { unsigned int bufferLength; int numBuffers; ERROR_CHECK(coreSystem->getDSPBufferSize(&bufferLength, &numBuffers)); return bufferLength; } int FmodServer::get_system_dsp_num_buffers() { unsigned int bufferLength; int numBuffers; ERROR_CHECK(coreSystem->getDSPBufferSize(&bufferLength, &numBuffers)); return numBuffers; } void FmodServer::pause_all_events() { for (const Ref& eventInstance : runningEvents) { eventInstance->set_paused(true); } } void FmodServer::unpause_all_events() { for (const Ref& eventInstance : runningEvents) { eventInstance->set_paused(false); } } void FmodServer::mute_all_events() { if (cache->is_master_loaded()) { FMOD::Studio::Bus* masterBus = nullptr; if (ERROR_CHECK(system->getBus("bus:/", &masterBus))) { masterBus->setMute(true); } } } void FmodServer::unmute_all_events() { if (cache->is_master_loaded()) { FMOD::Studio::Bus* masterBus = nullptr; if (ERROR_CHECK(system->getBus("bus:/", &masterBus))) { masterBus->setMute(false); } } } void FmodServer::mixer_suspend() { ERROR_CHECK(coreSystem->mixerSuspend()); } void FmodServer::mixer_resume() { ERROR_CHECK(coreSystem->mixerResume()); } Ref FmodServer::load_file_as_sound(const String& path) { if (cache->has_file(path)) { GODOT_LOG_WARNING("FMOD Sound System: FILE ALREADY LOADED AS SOUND" + String(path)) return cache->get_file(path); } return cache->add_file(path, FMOD_CREATESAMPLE); } Ref FmodServer::load_file_as_music(const String& path) { if (cache->has_file(path)) { GODOT_LOG_WARNING("FMOD Sound System: FILE ALREADY LOADED AS MUSIC" + String(path)) return cache->get_file(path); } return cache->add_file(path, (FMOD_CREATESTREAM | FMOD_LOOP_NORMAL)); } void FmodServer::unload_file(const String& path) { if (!cache->has_file(path)) { GODOT_LOG_WARNING("File " + path + " can't be found. Check if it was properly loaded or already unloaded.") return; } cache->remove_file(path); GODOT_LOG_VERBOSE("FMOD Sound System: UNLOADING FILE" + String(path)) } Ref FmodServer::create_sound_instance(const String& path) { if (!cache->has_file(path)) { GODOT_LOG_WARNING("File " + path + " can't be found. Check if it was properly loaded.") return {}; } Ref file = cache->get_file(path); FMOD::Channel* channel = nullptr; ERROR_CHECK_WITH_REASON(coreSystem->playSound(file->get_wrapped(), nullptr, true, &channel), vformat("Cannot play sound %s", path)); if (channel) { Ref ref = FmodSound::create_ref(channel); return ref; } return {}; } FMOD_STUDIO_SOUND_INFO FmodServer::get_sound_info(const String& sound_key) const { FMOD_STUDIO_SOUND_INFO sound_info; ERROR_CHECK_WITH_REASON(system->getSoundInfo(sound_key.utf8().get_data(), &sound_info), vformat("Cannot get sound info for %s", sound_key)); return sound_info; } FMOD::Sound* FmodServer::create_sound(FMOD_STUDIO_SOUND_INFO& sound_info, FMOD_MODE mode) const { FMOD::Sound* sound {nullptr}; ERROR_CHECK_WITH_REASON( coreSystem->createSound(sound_info.name_or_data, mode | sound_info.mode, &sound_info.exinfo, &sound), vformat("Cannot create sound %s with mode %s", sound_info.name_or_data, mode) ); return sound; } void FmodServer::set_sound_3d_settings(const Ref& p_settings) { float distance_factor = p_settings->get_distance_factor(); if (distance_factor <= 0) { GODOT_LOG_ERROR("FMOD Sound System: Failed to set 3D settings - invalid distance factor!") } else if (ERROR_CHECK(coreSystem->set3DSettings(p_settings->get_doppler_scale(), distance_factor, p_settings->get_rolloff_scale()))) { distanceScale = distance_factor; GODOT_LOG_VERBOSE("FMOD Sound System: Successfully set global 3D settings") } else { GODOT_LOG_ERROR("FMOD Sound System: Failed to set 3D settings") } } void FmodServer::wait_for_all_loads() { ERROR_CHECK(system->flushSampleLoading()); cache->update_pending(); } Array FmodServer::get_available_drivers() { Array driverList; int numDrivers = 0; ERROR_CHECK(coreSystem->getNumDrivers(&numDrivers)); for (int i = 0; i < numDrivers; ++i) { char name[MAX_DRIVER_NAME_SIZE]; int sampleRate; FMOD_SPEAKERMODE speakerMode; int speakerModeChannels; ERROR_CHECK(coreSystem->getDriverInfo(i, name, MAX_DRIVER_NAME_SIZE, nullptr, &sampleRate, &speakerMode, &speakerModeChannels)); String nameStr(name); Dictionary driverInfo; driverInfo["id"] = i; driverInfo["name"] = nameStr; driverInfo["sample_rate"] = sampleRate; driverInfo["speaker_mode"] = (int) speakerMode; driverInfo["number_of_channels"] = speakerModeChannels; driverList.push_back(driverInfo); } return driverList; } int FmodServer::get_driver() { int driverId = -1; ERROR_CHECK(coreSystem->getDriver(&driverId)); return driverId; } void FmodServer::set_driver(const int id) { ERROR_CHECK(coreSystem->setDriver(id)); } void FmodServer::_update_performance_data() { // get the CPU usage FMOD_STUDIO_CPU_USAGE studioCpuUsage; FMOD_CPU_USAGE cpuUsage; ERROR_CHECK(system->getCPUUsage(&studioCpuUsage, &cpuUsage)); performanceData->dsp = cpuUsage.dsp; performanceData->geometry = cpuUsage.geometry; performanceData->stream = cpuUsage.stream; performanceData->update = cpuUsage.update; performanceData->convolution1 = cpuUsage.convolution1; performanceData->convolution2 = cpuUsage.convolution2; performanceData->studio = studioCpuUsage.update; // get the memory usage ERROR_CHECK(FMOD::Memory_GetStats(&performanceData->currently_allocated, &performanceData->max_allocated)); // get the file usage long long sampleBytesRead = 0; long long streamBytesRead = 0; long long otherBytesRead = 0; ERROR_CHECK(coreSystem->getFileUsage(&sampleBytesRead, &streamBytesRead, &otherBytesRead)); performanceData->sample_bytes_read = static_cast(sampleBytesRead); performanceData->stream_bytes_read = static_cast(streamBytesRead); performanceData->other_bytes_read = static_cast(otherBytesRead); } Ref FmodServer::get_performance_data() { return performanceData; } void FmodServer::set_global_parameter_by_name(const String& parameter_name, float value) { ERROR_CHECK_WITH_REASON( system->setParameterByName(parameter_name.utf8().get_data(), value), vformat("Cannot set global parameter %s to value %f", parameter_name, value) ); } void FmodServer::set_global_parameter_by_name_with_label(const String& parameter_name, const String& label) { ERROR_CHECK_WITH_REASON( system->setParameterByNameWithLabel(parameter_name.utf8().get_data(), label.utf8().get_data()), vformat("Cannot set global parameter %s to value %s", parameter_name, label) ); } float FmodServer::get_global_parameter_by_name(const String& parameter_name) { float value = 0.f; ERROR_CHECK_WITH_REASON( system->getParameterByName(parameter_name.utf8().get_data(), &value), vformat("Cannot get global parameter %s", parameter_name, value) ); return value; } void FmodServer::set_global_parameter_by_id(uint64_t parameter_id, const float value) { ERROR_CHECK_WITH_REASON( system->setParameterByID(ulong_to_fmod_parameter_id(parameter_id), value), vformat("Cannot set global parameter %d to value %f", parameter_id, value) ); } void FmodServer::set_global_parameter_by_id_with_label(uint64_t parameter_id, const String& label) { ERROR_CHECK_WITH_REASON( system->setParameterByIDWithLabel(ulong_to_fmod_parameter_id(parameter_id), label.utf8().get_data()), vformat("Cannot set global parameter %d to value %s", parameter_id, label) ); } float FmodServer::get_global_parameter_by_id(uint64_t parameter_id) { float value = -1.f; ERROR_CHECK_WITH_REASON( system->getParameterByID(ulong_to_fmod_parameter_id(parameter_id), &value), vformat("Cannot set global parameter %d", parameter_id, value) ); return value; } Dictionary FmodServer::get_global_parameter_desc_by_name(const String& parameter_name) { Dictionary paramDesc; FMOD_STUDIO_PARAMETER_DESCRIPTION pDesc; if (ERROR_CHECK_WITH_REASON( system->getParameterDescriptionByName(parameter_name.utf8().get_data(), &pDesc), vformat("Cannot get global parameter %s", parameter_name) )) { paramDesc["name"] = String(pDesc.name); paramDesc["id_first"] = pDesc.id.data1; paramDesc["id_second"] = pDesc.id.data2; paramDesc["minimum"] = pDesc.minimum; paramDesc["maximum"] = pDesc.maximum; paramDesc["default_value"] = pDesc.defaultvalue; } return paramDesc; } Dictionary FmodServer::get_global_parameter_desc_by_id(uint64_t parameter_id) { Dictionary paramDesc; FMOD_STUDIO_PARAMETER_DESCRIPTION pDesc; if (ERROR_CHECK_WITH_REASON( system->getParameterDescriptionByID(ulong_to_fmod_parameter_id(parameter_id), &pDesc), vformat("Cannot get global parameter %d", parameter_id) )) { paramDesc["name"] = String(pDesc.name); paramDesc["id_first"] = pDesc.id.data1; paramDesc["id_second"] = pDesc.id.data2; paramDesc["minimum"] = pDesc.minimum; paramDesc["maximum"] = pDesc.maximum; paramDesc["default_value"] = pDesc.defaultvalue; } return paramDesc; } int FmodServer::get_global_parameter_desc_count() { int count = 0; ERROR_CHECK(system->getParameterDescriptionCount(&count)); return count; } Array FmodServer::get_global_parameter_desc_list() { Array a; FMOD_STUDIO_PARAMETER_DESCRIPTION descList[256]; int count = 0; ERROR_CHECK(system->getParameterDescriptionList(descList, 256, &count)); for (int i = 0; i < count; ++i) { auto pDesc = descList[i]; Dictionary paramDesc; paramDesc["name"] = String(pDesc.name); paramDesc["id_first"] = pDesc.id.data1; paramDesc["id_second"] = pDesc.id.data2; paramDesc["minimum"] = pDesc.minimum; paramDesc["maximum"] = pDesc.maximum; paramDesc["default_value"] = pDesc.defaultvalue; a.append(paramDesc); } return a; } void FmodServer::add_callback(const Callback& callback) { callback_mutex->lock(); callbacks_to_process.push_back(callback); callback_mutex->unlock(); } void FmodServer::_apply_parameter_dict_to_event(const Ref& p_event, const Dictionary& parameters) { Array keys = parameters.keys(); for (int i = 0; i < keys.size(); ++i) { Variant& key = keys[i]; const Variant& value = parameters[keys[i]]; if (key.get_type() == Variant::Type::INT) { if (value.get_type() == Variant::Type::STRING) { p_event->set_parameter_by_id_with_label(key, value); continue; } p_event->set_parameter_by_id(key, value); continue; } if (value.get_type() == Variant::Type::STRING) { p_event->set_parameter_by_name_with_label(key, value); continue; } p_event->set_parameter_by_name(key, value); } } ================================================ FILE: src/fmod_server.h ================================================ #ifndef GODOTFMOD_FMOD_SERVER_H #define GODOTFMOD_FMOD_SERVER_H #include "core/fmod_file.h" #include "core/fmod_sound.h" #include "data/performance_data.h" #include "fmod_cache.h" #include "resources/fmod_plugins_settings.h" #include "studio/fmod_bank.h" #include "studio/fmod_bus.h" #include "studio/fmod_event.h" #include "studio/fmod_event_description.h" #include "studio/fmod_vca.h" #include "templates/hash_map.hpp" #include "templates/local_vector.hpp" #include "templates/vector.hpp" #include "variant/string.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace godot { struct OneShot { NodeWrapper wrapper; Ref instance; }; struct Listener { NodeWrapper wrapper; bool listenerLock = false; float weight = 1.0; }; struct Callback { FMOD_STUDIO_EVENT_CALLBACK_TYPE type; godot::Callable callable; Dictionary fmod_callback_properties; }; class FmodServer : public Object { GDCLASS(FmodServer, Object); static FmodServer* singleton; public: union EventIdentifier { const char* string_identifier; FMOD_GUID guid; FmodEventDescription* event_description; }; enum EventIdentifierType { PATH, GUID, EVENT_DESCRIPTION }; union ParameterIdentifier { String* name; uint64_t id; }; struct ParameterValue { ParameterIdentifier identifier; Variant value; Variant::Type variant_type; bool should_load_by_id; }; private: FMOD::Studio::System* system; FMOD::System* coreSystem; bool isInitialized; bool isNotInitializedPrinted; float distanceScale; FmodCache* cache; int systemListenerNumber = 1; Listener listeners[FMOD_MAX_LISTENERS]; Vector oneShots; Vector> runningEvents; Ref performanceData; // Direct call from fmod thread cannot be made as we cannot interact with scene tree from another thread (thread // guard). // call_deferred is not implemented in godot-cpp. // Would have prefered a SpinLock but does not seems to exist in godot-cpp. // TODO: Change when https://github.com/godotengine/godot-cpp/pull/1091 is merged. Ref callback_mutex; List callbacks_to_process; void _set_listener_attributes(); Ref _get_event_description(const String& event_name); Ref _get_event_description(const FMOD_GUID& guid); void _update_performance_data(); public: FmodServer(); ~FmodServer() override; static FmodServer* get_singleton(); // LIFECYCLE void init(const Ref& p_settings); void update(); void shutdown(); // SETTINGS void set_software_format(const Ref& p_settings); void set_sound_3d_settings(const Ref& p_settings); void set_system_dsp_buffer_size(const Ref& p_settings); Ref get_system_dsp_buffer_settings(); unsigned int get_system_dsp_buffer_length(); int get_system_dsp_num_buffers(); // OBJECTS bool check_vca_guid(const String& guid); bool check_vca_path(const String& vcaPath); bool check_bus_guid(const String& guid); bool check_bus_path(const String& busPath); bool check_event_guid(const String& guid); bool check_event_guid_internal(const FMOD_GUID& guid); bool check_event_path(const String& eventPath); Ref get_vca_from_guid(const String& guid); Ref get_vca(const String& vcaPath); Ref get_bus_from_guid(const String& guid); Ref get_bus(const String& busPath); Ref get_event_from_guid(const String& guid); Ref get_event_from_guid_internal(const FMOD_GUID& guid); Ref get_event(const String& eventPath); FMOD_GUID get_event_guid_internal(const String& event_path); String get_event_guid(const String& event_path); String get_event_path_internal(const FMOD_GUID& guid); String get_event_path(const String& guid); Array get_all_vca(); Array get_all_buses(); Array get_all_event_descriptions(); Array get_all_banks(); // DEBUGGING Array get_available_drivers(); int get_driver(); void set_driver(int id); Ref get_performance_data(); // GLOBAL PARAMETERS void set_global_parameter_by_name(const String& parameter_name, float value); void set_global_parameter_by_name_with_label(const String& parameter_name, const String& label); float get_global_parameter_by_name(const String& parameter_name); void set_global_parameter_by_id(uint64_t parameter_id, float value); void set_global_parameter_by_id_with_label(uint64_t parameter_id, const String& label); float get_global_parameter_by_id(uint64_t parameter_id); Dictionary get_global_parameter_desc_by_name(const String& parameter_name); Dictionary get_global_parameter_desc_by_id(uint64_t parameter_id); int get_global_parameter_desc_count(); Array get_global_parameter_desc_list(); // LISTENERS void add_listener(int index, Node* game_obj); void remove_listener(int index, Node* game_obj); void set_system_listener_number(int listenerNumber); int get_system_listener_number() const; float get_system_listener_weight(int index); void set_system_listener_weight(int index, float weight); Transform3D get_listener_transform3d(int index); Transform2D get_listener_transform2d(int index); Vector3 get_listener_3d_velocity(int index); Vector2 get_listener_2d_velocity(int index); void set_listener_transform3d(int index, const Transform3D& transform); void set_listener_transform2d(int index, const Transform2D& transform); void set_listener_lock(int index, bool isLocked); bool get_listener_lock(int index); Object* get_object_attached_to_listener(int index); // BANKS Ref load_bank(const String& pathToBank, unsigned int flag); void unload_bank(const String& pathToBank); bool banks_still_loading(); // PLUGINS void load_all_plugins(const Ref& p_settings); uint32_t load_plugin(const String& p_plugin_path, uint32_t p_priority = 0); void unload_plugin(uint32_t p_plugin_handle); bool is_plugin_loaded(uint32_t p_plugin_handle); // EVENTS private: template Ref _create_event_instance(const EventIdentifier& identifier); public: Ref create_event_instance_with_guid(const String& guid); Ref create_event_instance_with_guid_internal(const FMOD_GUID& guid); Ref create_event_instance(const String& eventPath); Ref create_event_instance_from_description(const Ref& event_description); // SOUNDS Ref load_file_as_sound(const String& path); Ref load_file_as_music(const String& path); void unload_file(const String& path); Ref create_sound_instance(const String& path); FMOD_STUDIO_SOUND_INFO get_sound_info(const String& sound_key) const; FMOD::Sound* create_sound(FMOD_STUDIO_SOUND_INFO& sound_info, FMOD_MODE mode) const; //CALLBACKS void add_callback(const Callback& callback); /* Helper methods */ private: template Ref fetch_event_description(const EventIdentifier& identifier); template void _play_one_shot(const EventIdentifier& identifier, Node* game_obj, const Dictionary& parameters = Dictionary()); static void _apply_parameter_dict_to_event(const Ref& p_event, const Dictionary& parameters); public: template void apply_parameter_list_to_event(const Ref& p_event, const LocalVector& parameters); void play_one_shot(const String& event_name); void play_one_shot_with_params(const String& event_name, const Dictionary& parameters); void play_one_shot_attached(const String& event_name, Node* game_obj); void play_one_shot_attached_with_params(const String& event_name, Node* game_obj, const Dictionary& parameters); void play_one_shot_using_guid(const String& guid); void play_one_shot_using_guid_with_params(const String& guid, const Dictionary& parameters); void play_one_shot_using_guid_attached(const String& guid, Node* game_obj); void play_one_shot_using_guid_attached_with_params(const String& guid, Node* game_obj, const Dictionary& parameters); void play_one_shot_using_event_description(const Ref& event_description); void play_one_shot_using_event_description_with_params(const Ref& event_description, const Dictionary& parameters); void play_one_shot_using_event_description_attached(const Ref& event_description, Node* game_obj); void play_one_shot_using_event_description_attached_with_params(const Ref& event_description, Node* game_obj, const Dictionary& parameters); void pause_all_events(); void unpause_all_events(); void mute_all_events(); void unmute_all_events(); void mixer_suspend(); void mixer_resume(); void wait_for_all_loads(); protected: static void _bind_methods(); }; template Ref FmodServer::fetch_event_description(const FmodServer::EventIdentifier& identifier){ Ref desc; switch (parameter_type) { case PATH: desc = _get_event_description(identifier.string_identifier); break; case GUID: desc = _get_event_description(identifier.guid); break; case EVENT_DESCRIPTION: desc = Ref(identifier.event_description); break; } return desc; } template Ref FmodServer::_create_event_instance(const EventIdentifier& identifier) { FMOD::Studio::EventInstance* eventInstance = nullptr; ERROR_CHECK(fetch_event_description(identifier)->get_wrapped()->createInstance(&eventInstance)); Ref ref = FmodEvent::create_ref(eventInstance); if (ref.is_null() || !ref->is_valid()) { GODOT_LOG_WARNING("Event Instance is invalid.") return {}; } ref->get_wrapped()->setUserData(ref.ptr()); ref->set_distance_scale(distanceScale); runningEvents.push_back(ref); return ref; } template void FmodServer::_play_one_shot(const FmodServer::EventIdentifier& identifier, Node* game_obj, const Dictionary& parameters) { Ref desc = fetch_event_description(identifier); if (!desc->is_one_shot()){ GODOT_LOG_WARNING(desc->get_path() + " is not a OneShot event.") return; } Ref ref = _create_event_instance(identifier); if (ref.is_null()) { return; } if (!parameters.is_empty()) { // set the initial parameter values _apply_parameter_dict_to_event(ref, parameters); } if(game_obj){ auto* oneShot = new OneShot {NodeWrapper {game_obj}, ref}; ref->set_node_attributes(game_obj); oneShots.push_back(oneShot); } ref->start(); ref->release(); } template void FmodServer::apply_parameter_list_to_event(const Ref& p_event, const LocalVector& parameters) { for (const TParameter& parameter : parameters) { if (parameter.should_load_by_id) { if (parameter.variant_type == Variant::Type::STRING) { p_event->set_parameter_by_id_with_label(parameter.identifier.id, parameter.value); continue; } p_event->set_parameter_by_id(parameter.identifier.id, parameter.value); continue; } if (parameter.variant_type == Variant::Type::STRING) { p_event->set_parameter_by_name_with_label(*parameter.identifier.name, parameter.value); continue; } p_event->set_parameter_by_name(*parameter.identifier.name, parameter.value); } } }// namespace godot #endif// GODOTFMOD_FMOD_SERVER_H ================================================ FILE: src/fmod_string_names.cpp ================================================ #include "fmod_string_names.h" using namespace godot; FmodStringNames* FmodStringNames::instance = nullptr; void FmodStringNames::create() { instance = memnew(FmodStringNames); } void FmodStringNames::free() { memdelete(instance); instance = nullptr; } FmodStringNames* FmodStringNames::get_instance() { return instance; } FmodStringNames::FmodStringNames() : bank_path_property_name("bank_paths"), event_parameter_prefix_for_properties(EVENT_PARAMETER_PREFIX_FOR_PROPERTIES) { } ================================================ FILE: src/fmod_string_names.h ================================================ #ifndef GODOTFMOD_FMOD_STRING_NAMES_H #define GODOTFMOD_FMOD_STRING_NAMES_H #include "godot.hpp" #include "variant/string_name.hpp" using namespace godot; class FmodStringNames { friend void initialize_fmod_module(ModuleInitializationLevel p_level); friend void uninitialize_fmod_module(ModuleInitializationLevel p_level); static void create(); static void free(); static FmodStringNames* instance; FmodStringNames(); public: static FmodStringNames* get_instance(); static constexpr const char* EVENT_PARAMETER_PREFIX_FOR_PROPERTIES = "fmod_parameters"; StringName bank_path_property_name; StringName event_parameter_prefix_for_properties; }; #endif //GODOTFMOD_FMOD_STRING_NAMES_H ================================================ FILE: src/helpers/common.h ================================================ #ifndef GODOTFMOD_COMMON_H #define GODOTFMOD_COMMON_H #include "classes/canvas_item.hpp" #include "classes/node3d.hpp" #include "fmod_common.h" #include "variant/utility_functions.hpp" #include #include #include #include #include #include #define MAX_PATH_SIZE 512 #define MAX_DRIVER_NAME_SIZE 256 #define GODOT_LOG_INFO(message) UtilityFunctions::print(message); #define GODOT_LOG_VERBOSE(message) UtilityFunctions::print_verbose(message); #define GODOT_LOG_WARNING(message) UtilityFunctions::push_warning(message, BOOST_CURRENT_FUNCTION, __FILE__, __LINE__); #define GODOT_LOG_ERROR(message) UtilityFunctions::push_error(message, BOOST_CURRENT_FUNCTION, __FILE__, __LINE__); #define ERROR_CHECK_WITH_REASON(_result, _reason) \ (((_result) != FMOD_OK) ? (godot::UtilityFunctions::push_error(_reason, BOOST_CURRENT_FUNCTION, __FILE__, __LINE__), false) : true) #define ERROR_CHECK(_result) ((_result) == FMOD_OK) #define FMODCLASS(m_class, m_inherits, m_owned) \ GDCLASS(m_class, m_inherits) \ \ private: \ m_owned* _wrapped = nullptr; \ \ public: \ inline static Ref create_ref(m_owned* wrapped) { \ Ref ref; \ if (wrapped) { \ ref.instantiate(); \ ref->_wrapped = wrapped; \ } \ return ref; \ } \ \ bool is_valid() const { \ return _wrapped != nullptr && _wrapped->isValid(); \ } \ \ m_owned* get_wrapped() const { \ return _wrapped; \ } \ \ private: #define FMODCLASSWITHPATH(m_class, m_inherits, m_owned) \ GDCLASS(m_class, m_inherits) \ \ private: \ m_owned* _wrapped = nullptr; \ FMOD_GUID _guid; \ String _path; \ \ public: \ inline static Ref create_ref(m_owned* wrapped) { \ Ref ref; \ if (wrapped) { \ ref.instantiate(); \ ref->_wrapped = wrapped; \ char path[MAX_PATH_SIZE]; \ ERROR_CHECK(wrapped->getPath(path, MAX_PATH_SIZE, nullptr)); \ ERROR_CHECK(wrapped->getID(&ref->_guid)); \ ref->_path = String(path); \ } \ return ref; \ } \ \ bool is_valid() const { \ return _wrapped != nullptr && _wrapped->isValid(); \ } \ \ m_owned* get_wrapped() const { \ return _wrapped; \ } \ \ String get_path() const { \ return _path; \ } \ \ FMOD_GUID get_guid() const { \ return _guid; \ } \ \ String get_guid_as_string() const { \ return fmod_guid_to_string(_guid); \ } \ \ private: namespace godot { class NodeWrapper { Node* node {nullptr}; ObjectID id; _FORCE_INLINE_ static bool is_spatial_node(Object* p_object) { if (Node::cast_to(p_object) || Node::cast_to(p_object)) { return true; } GODOT_LOG_ERROR("Invalid Object. A Godot object bound to FMOD has to be either a Node3D or CanvasItem.") return false; } public: bool is_valid() const { if (!node || !id.is_valid() || !UtilityFunctions::is_instance_id_valid(id)) { return false; } return node->is_inside_tree(); } Node* get_node() { return node; } void set_node(Node* p_node) { if (p_node) { if (is_spatial_node(p_node)) { node = p_node; id = p_node->get_instance_id(); return; } } node = nullptr; id = ObjectID(); } NodeWrapper() = default; explicit NodeWrapper(Node* p_node) { set_node(p_node); }; }; template static inline Ref create_ref() { Ref ref; ref.instantiate(); return ref; } static inline FMOD_GUID string_to_fmod_guid(const char* guid) { FMOD_GUID result; sscanf(guid, "{%8x-%4hx-%4hx-%2hhx%2hhx-%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx}", &result.Data1, &result.Data2, &result.Data3, &result.Data4[0], &result.Data4[1], &result.Data4[2], &result.Data4[3], &result.Data4[4], &result.Data4[5], &result.Data4[6], &result.Data4[7]); return result; } static inline String fmod_guid_to_string(const FMOD_GUID& guid) { char result[39]; snprintf(result, sizeof(result), "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); return result; } static inline uint64_t fmod_parameter_id_to_ulong(const FMOD_STUDIO_PARAMETER_ID& parameter_id) { const unsigned int first_id_part {parameter_id.data1}; return (static_cast(first_id_part) << 32) | static_cast(parameter_id.data2); } static inline FMOD_STUDIO_PARAMETER_ID ulong_to_fmod_parameter_id(uint64_t converted) { FMOD_STUDIO_PARAMETER_ID paramId; paramId.data2 = static_cast(converted & 0xFFFFFFFF); paramId.data1 = static_cast((converted >> 32) & 0xFFFFFFFF); return paramId; } static inline bool equals(const FMOD_GUID& first, const FMOD_GUID& second) { return first.Data1 == second.Data1 && first.Data2 == second.Data2 && first.Data3 == second.Data3 && first.Data4[0] == second.Data4[0] && first.Data4[1] == second.Data4[1] && first.Data4[2] == second.Data4[2] && first.Data4[3] == second.Data4[3] && first.Data4[4] == second.Data4[4] && first.Data4[5] == second.Data4[5] && first.Data4[6] == second.Data4[6] && first.Data4[7] == second.Data4[7]; } }// namespace godot #endif// GODOTFMOD_COMMON_H ================================================ FILE: src/helpers/constants.h ================================================ #ifndef GODOTFMOD_BIND_CONSTANTS_H #define GODOTFMOD_BIND_CONSTANTS_H #include #include #define REGISTER_ALL_CONSTANTS \ BIND_CONSTANT(FMOD_INIT_3D_RIGHTHANDED) \ BIND_CONSTANT(FMOD_INIT_CHANNEL_DISTANCEFILTER) \ BIND_CONSTANT(FMOD_INIT_CHANNEL_LOWPASS) \ BIND_CONSTANT(FMOD_INIT_GEOMETRY_USECLOSEST) \ BIND_CONSTANT(FMOD_INIT_MIX_FROM_UPDATE) \ BIND_CONSTANT(FMOD_INIT_NORMAL) \ BIND_CONSTANT(FMOD_INIT_PREFER_DOLBY_DOWNMIX) \ BIND_CONSTANT(FMOD_INIT_PROFILE_ENABLE) \ BIND_CONSTANT(FMOD_INIT_PROFILE_METER_ALL) \ BIND_CONSTANT(FMOD_INIT_STREAM_FROM_UPDATE) \ BIND_CONSTANT(FMOD_INIT_THREAD_UNSAFE) \ BIND_CONSTANT(FMOD_INIT_VOL0_BECOMES_VIRTUAL) \ \ BIND_CONSTANT(FMOD_STUDIO_INIT_NORMAL) \ BIND_CONSTANT(FMOD_STUDIO_INIT_LIVEUPDATE) \ BIND_CONSTANT(FMOD_STUDIO_INIT_ALLOW_MISSING_PLUGINS) \ BIND_CONSTANT(FMOD_STUDIO_INIT_SYNCHRONOUS_UPDATE) \ BIND_CONSTANT(FMOD_STUDIO_INIT_DEFERRED_CALLBACKS) \ BIND_CONSTANT(FMOD_STUDIO_INIT_LOAD_FROM_UPDATE) \ \ BIND_CONSTANT(FMOD_SPEAKERMODE_5POINT1) \ BIND_CONSTANT(FMOD_SPEAKERMODE_7POINT1) \ BIND_CONSTANT(FMOD_SPEAKERMODE_7POINT1POINT4) \ BIND_CONSTANT(FMOD_SPEAKERMODE_DEFAULT) \ BIND_CONSTANT(FMOD_SPEAKERMODE_MAX) \ BIND_CONSTANT(FMOD_SPEAKERMODE_MONO) \ BIND_CONSTANT(FMOD_SPEAKERMODE_QUAD) \ BIND_CONSTANT(FMOD_SPEAKERMODE_RAW) \ BIND_CONSTANT(FMOD_SPEAKERMODE_STEREO) \ BIND_CONSTANT(FMOD_SPEAKERMODE_SURROUND) \ \ BIND_CONSTANT(FMOD_STUDIO_LOAD_BANK_NORMAL) \ BIND_CONSTANT(FMOD_STUDIO_LOAD_BANK_NONBLOCKING) \ BIND_CONSTANT(FMOD_STUDIO_LOAD_BANK_DECOMPRESS_SAMPLES) \ \ BIND_CONSTANT(FMOD_2D) \ BIND_CONSTANT(FMOD_3D) \ BIND_CONSTANT(FMOD_3D_CUSTOMROLLOFF) \ BIND_CONSTANT(FMOD_3D_HEADRELATIVE) \ BIND_CONSTANT(FMOD_3D_IGNOREGEOMETRY) \ BIND_CONSTANT(FMOD_3D_INVERSEROLLOFF) \ BIND_CONSTANT(FMOD_3D_INVERSETAPEREDROLLOFF) \ BIND_CONSTANT(FMOD_3D_LINEARROLLOFF) \ BIND_CONSTANT(FMOD_3D_LINEARSQUAREROLLOFF) \ BIND_CONSTANT(FMOD_3D_WORLDRELATIVE) \ BIND_CONSTANT(FMOD_ACCURATETIME) \ BIND_CONSTANT(FMOD_CREATECOMPRESSEDSAMPLE) \ BIND_CONSTANT(FMOD_CREATESAMPLE) \ BIND_CONSTANT(FMOD_CREATESTREAM) \ BIND_CONSTANT(FMOD_DEFAULT) \ BIND_CONSTANT(FMOD_IGNORETAGS) \ BIND_CONSTANT(FMOD_LOOP_BIDI) \ BIND_CONSTANT(FMOD_LOOP_NORMAL) \ BIND_CONSTANT(FMOD_LOOP_OFF) \ BIND_CONSTANT(FMOD_LOWMEM) \ BIND_CONSTANT(FMOD_MPEGSEARCH) \ BIND_CONSTANT(FMOD_NONBLOCKING) \ BIND_CONSTANT(FMOD_OPENMEMORY) \ BIND_CONSTANT(FMOD_OPENMEMORY_POINT) \ BIND_CONSTANT(FMOD_OPENONLY) \ BIND_CONSTANT(FMOD_OPENRAW) \ BIND_CONSTANT(FMOD_OPENUSER) \ BIND_CONSTANT(FMOD_UNIQUE) \ BIND_CONSTANT(FMOD_VIRTUAL_PLAYFROMSTART) \ \ BIND_CONSTANT(FMOD_STUDIO_STOP_ALLOWFADEOUT) \ BIND_CONSTANT(FMOD_STUDIO_STOP_IMMEDIATE) \ BIND_CONSTANT(FMOD_STUDIO_STOP_FORCEINT) \ \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_PREUPDATE) \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_POSTUPDATE) \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD) \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_LIVEUPDATE_CONNECTED) \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_LIVEUPDATE_DISCONNECTED) \ BIND_CONSTANT(FMOD_STUDIO_SYSTEM_CALLBACK_ALL) \ \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_CREATED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_DESTROYED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_STARTING) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_STARTED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_RESTARTED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_STOPPED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_START_FAILED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_CREATE_PROGRAMMER_SOUND) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_DESTROY_PROGRAMMER_SOUND) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_PLUGIN_CREATED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_PLUGIN_DESTROYED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_SOUND_PLAYED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_SOUND_STOPPED) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_REAL_TO_VIRTUAL) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_VIRTUAL_TO_REAL) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_START_EVENT_COMMAND) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_NESTED_TIMELINE_BEAT) \ BIND_CONSTANT(FMOD_STUDIO_EVENT_CALLBACK_ALL) \ \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_UNLOADING) \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_UNLOADED) \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_LOADING) \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_LOADED) \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_ERROR) \ BIND_CONSTANT(FMOD_STUDIO_LOADING_STATE_FORCEINT) \ \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_PLAYING) \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_SUSTAINING) \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_STOPPED) \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_STARTING) \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_STOPPING) \ BIND_CONSTANT(FMOD_STUDIO_PLAYBACK_FORCEINT) #endif// GODOTFMOD_BIND_CONSTANTS_H ================================================ FILE: src/helpers/current_function.h ================================================ #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED #define BOOST_CURRENT_FUNCTION_HPP_INCLUDED // MS compatible compilers support #pragma once #if defined(_MSC_VER) && (_MSC_VER >= 1020) #pragma once #endif // // boost/current_function.hpp - BOOST_CURRENT_FUNCTION // // Copyright (c) 2002 Peter Dimov and Multi Media Ltd. // // Distributed under the Boost Software License, Version 1.0. (See // accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) // // http://www.boost.org/libs/utility/current_function.html // namespace boost { namespace detail { inline void current_function_helper() { #if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) #define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ #elif defined(__DMC__) && (__DMC__ >= 0x810) #define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ #elif defined(__FUNCSIG__) #define BOOST_CURRENT_FUNCTION __FUNCSIG__ #elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500)) #define BOOST_CURRENT_FUNCTION __FUNCTION__ #elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550) #define BOOST_CURRENT_FUNCTION __FUNC__ #elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901) #define BOOST_CURRENT_FUNCTION __func__ #else #define BOOST_CURRENT_FUNCTION "(unknown)" #endif } }// namespace detail }// namespace boost #endif// #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED ================================================ FILE: src/helpers/files.h ================================================ #ifndef GODOTFMOD_FILES_H #define GODOTFMOD_FILES_H #include #include namespace godot { static void list_files_in_folder(PackedStringArray& result, const String& folder, const String& extension = "", const PackedStringArray& excluded_folders = PackedStringArray()) { for (const String& excluded : excluded_folders) { if (folder == excluded) { return; } } Ref folder_access {DirAccess::open(folder)}; if (folder_access.is_null()) { return; } folder_access->list_dir_begin(); String current_file{folder_access->get_next()}; while (!current_file.is_empty()) { if (current_file == ".." || current_file == ".") { current_file = folder_access->get_next(); continue; } String current_file_path{vformat("%s/%s", folder, current_file)}; if (folder_access->current_is_dir()) { list_files_in_folder(result, current_file_path, extension, excluded_folders); } else if (current_file.ends_with(extension)) { result.push_back(current_file_path); } current_file = folder_access->get_next(); } folder_access->list_dir_end(); } } #endif// GODOTFMOD_FILES_H ================================================ FILE: src/helpers/maths.h ================================================ #ifndef GODOTFMOD_MATHS_H #define GODOTFMOD_MATHS_H #include "fmod_common.h" #include "variant/dictionary.hpp" #include "variant/transform2d.hpp" #include "variant/transform3d.hpp" #include "variant/vector2.hpp" #include "variant/vector3.hpp" namespace godot { static inline FMOD_VECTOR get_fmod_vector_from_3d(const Vector3& vec) { FMOD_VECTOR fv; fv.x = vec.x; fv.y = vec.y; fv.z = vec.z; return fv; } static inline FMOD_3D_ATTRIBUTES get_3d_attributes(const FMOD_VECTOR& pos, const FMOD_VECTOR& up, const FMOD_VECTOR& forward, const FMOD_VECTOR& vel) { FMOD_3D_ATTRIBUTES f3d; f3d.forward = forward; f3d.position = pos; f3d.up = up; f3d.velocity = vel; return f3d; } static inline FMOD_3D_ATTRIBUTES get_3d_attributes_from_transform3d(const Transform3D& transform, const Vector3& velocity, const float distanceScale) { Vector3 pos = transform.get_origin() / distanceScale; Vector3 up = transform.get_basis().get_column(1).normalized();; Vector3 forward = -transform.get_basis().get_column(2).normalized();; return get_3d_attributes(get_fmod_vector_from_3d(pos), get_fmod_vector_from_3d(up), get_fmod_vector_from_3d(forward), get_fmod_vector_from_3d(velocity)); } static inline FMOD_3D_ATTRIBUTES get_3d_attributes_from_transform3d(const Transform3D& transform, const float distanceScale) { Vector3 pos = transform.get_origin() / distanceScale; Vector3 up = transform.get_basis().get_column(1).normalized(); Vector3 forward = -transform.get_basis().get_column(2).normalized(); Vector3 vel(0, 0, 0); return get_3d_attributes(get_fmod_vector_from_3d(pos), get_fmod_vector_from_3d(up), get_fmod_vector_from_3d(forward), get_fmod_vector_from_3d(vel)); } static inline FMOD_3D_ATTRIBUTES get_3d_attributes_from_transform2d(const Transform2D& transform, const Vector2& velocity, const float distanceScale) { Vector2 posVector = transform.get_origin() / distanceScale; Vector3 pos = Vector3(-posVector.x, 0.0f, posVector.y); Vector3 up = Vector3(0, 1, 0).normalized(); Vector3 forward = Vector3(-transform.columns[1].x, 0, transform.columns[1].y).normalized(); Vector3 vel(-velocity.x, 0, velocity.y); return get_3d_attributes(get_fmod_vector_from_3d(pos), get_fmod_vector_from_3d(up), get_fmod_vector_from_3d(forward), get_fmod_vector_from_3d(vel)); } static inline FMOD_3D_ATTRIBUTES get_3d_attributes_from_transform2d(const Transform2D& transform, const float distanceScale) { Vector2 posVector = transform.get_origin() / distanceScale; Vector3 pos(-posVector.x, 0.0f, posVector.y); Vector3 up(0, 1, 0); Vector3 forward = Vector3(-transform.columns[1].x, 0, transform.columns[1].y).normalized(); Vector3 vel(0, 0, 0); const FMOD_VECTOR& posFmodVector = get_fmod_vector_from_3d(pos); return get_3d_attributes(posFmodVector, get_fmod_vector_from_3d(up), get_fmod_vector_from_3d(forward), get_fmod_vector_from_3d(vel)); } static inline Transform3D get_transform3d_from_3d_attributes(FMOD_3D_ATTRIBUTES& attr, const float distanceScale) { Transform3D transform; transform.origin = Vector3(attr.position.x, attr.position.y, attr.position.z) * distanceScale; const Vector3& upVector = Vector3(attr.up.x, attr.up.y, attr.up.z); transform.basis.set_column(1, upVector); const Vector3& forwardVector = Vector3(attr.forward.x, attr.forward.y, attr.forward.z); transform.basis.set_column(2, forwardVector); transform.basis.set_column(0, upVector.cross(forwardVector)); return transform; } static inline Transform2D get_transform2d_from_3d_attributes(FMOD_3D_ATTRIBUTES& attr, const float distanceScale) { Transform2D transform; transform.set_origin(Vector2(-attr.position.x, attr.position.z) * distanceScale); const Vector2& forward = Vector2(-attr.forward.x, attr.forward.z); transform.columns[1] = forward; transform.columns[0] = Vector2(forward.y, forward.x); return transform; } static inline Vector3 get_velocity3d_from_3d_attributes(FMOD_3D_ATTRIBUTES& attr, const float distanceScale) { return {attr.velocity.x, attr.velocity.y, attr.velocity.z}; } static inline Vector2 get_velocity2d_from_3d_attributes(FMOD_3D_ATTRIBUTES& attr, const float distanceScale) { return {-attr.velocity.x, attr.velocity.z}; } }// namespace godot #endif// GODOTFMOD_MATHS_H ================================================ FILE: src/nodes/fmod_bank_loader.cpp ================================================ #include "fmod_bank_loader.h" #include "fmod_server.h" #include "fmod_string_names.h" #include using namespace godot; void FmodBankLoader::_enter_tree() { #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { return; } #endif for (int i = 0; i < bank_paths.size(); ++i) { bank.append( FmodServer::get_singleton()->load_bank(bank_paths[i], FMOD_STUDIO_LOAD_BANK_NORMAL) ); } } void FmodBankLoader::set_bank_paths(const Array& p_paths) { bank_paths = p_paths; } const Array& FmodBankLoader::get_bank_paths() const { return bank_paths; } bool FmodBankLoader::_property_can_revert(const StringName& p_property) const { if (p_property == FmodStringNames::get_instance()->bank_path_property_name) { return true; } return false; } bool FmodBankLoader::_property_get_revert(const StringName& p_property, Variant& result) const { if (p_property != FmodStringNames::get_instance()->bank_path_property_name) { return false; } result = Array(); return true; } void godot::FmodBankLoader::_bind_methods() { ClassDB::bind_method(D_METHOD("set_bank_paths", "p_paths"), &FmodBankLoader::set_bank_paths); ClassDB::bind_method(D_METHOD("get_bank_paths"), &FmodBankLoader::get_bank_paths); ADD_PROPERTY( PropertyInfo( Variant::ARRAY, FmodStringNames::get_instance()->bank_path_property_name, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_bank_paths", "get_bank_paths" ); } ================================================ FILE: src/nodes/fmod_bank_loader.h ================================================ #ifndef GODOTFMOD_FMOD_BANK_LOADER_H #define GODOTFMOD_FMOD_BANK_LOADER_H #include "classes/node.hpp" #include "studio/fmod_bank.h" namespace godot { class FmodBankLoader : public Node { GDCLASS(FmodBankLoader, Node) public: virtual void _enter_tree() override; void set_bank_paths(const Array& p_paths); const Array& get_bank_paths() const; bool _property_can_revert(const StringName& p_property) const; bool _property_get_revert(const StringName& p_property, Variant& result) const; private: Vector> bank; Array bank_paths; public: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_BANK_LOADER_H ================================================ FILE: src/nodes/fmod_event_emitter.h ================================================ #ifndef GODOTFMOD_FMOD_EVENT_EMITTER_H #define GODOTFMOD_FMOD_EVENT_EMITTER_H #include "classes/object.hpp" #include #include #include #include #include static constexpr const char* BEAT_SIGNAL_STRING = "timeline_beat"; static constexpr const char* MARKER_SIGNAL_STRING = "timeline_marker"; static constexpr const char* START_FAILED_SIGNAL_STRING = "start_failed"; static constexpr const char* STARTED_SIGNAL_STRING = "started"; static constexpr const char* RESTARTED_SIGNAL_STRING = "restarted"; static constexpr const char* STOPPED_SIGNAL_STRING = "stopped"; namespace godot { template class FmodEventEmitter : public NodeType { struct Parameter : public FmodServer::ParameterValue { String name; uint64_t id; PackedStringArray labels; bool operator==(const Parameter& parameter) const { return id == parameter.id; } }; mutable Ref _event_description; Ref _event; String _event_name; String _programmer_callback_sound_key; LocalVector _parameters; FMOD_GUID _event_guid; float _volume = 1.0; bool _attached = true; bool _autoplay = false; bool _auto_release = false; bool _allow_fadeout = true; bool _preload_event = true; void ready(); void process(); void exit_tree(); public: void _notification(int p_what); void play(bool restart_if_playing = true); void stop(); void play_one_shot(); Variant get_parameter(const String& p_name) const; void set_parameter(const String& p_name, const Variant& p_property); Variant get_parameter_by_id(uint64_t p_id) const; void set_parameter_by_id(uint64_t p_id, const Variant& p_property); void set_paused(bool p_is_paused); const Ref& get_event() const; bool is_paused(); void set_event_name(const String& name); String get_event_name() const; void set_event_guid(const String& guid); String get_event_guid() const; void set_attached(bool attached); bool is_attached() const; void set_autoplay(bool autoplay); bool is_autoplay() const; void set_auto_release(bool auto_release); bool is_auto_release() const; void set_allow_fadeout(bool allow_fadeout); bool is_allow_fadeout() const; void set_preload_event(bool preload_event); bool is_preload_event() const; void set_volume(float volume); float get_volume() const; void set_programmer_callback(const String& p_programmers_callback_sound_key); #ifdef TOOLS_ENABLED void tool_remove_all_parameters(); void tool_remove_parameter(uint64_t parameter_id); #endif static const StringName& get_class_static(); protected: void _emit_callbacks(const Dictionary& dict, int type) const; bool _set(const StringName& p_name, const Variant& p_property); bool _get(const StringName& p_name, Variant& r_property) const; bool _property_can_revert(const StringName& p_name) const; bool _property_get_revert(const StringName& p_name, Variant& r_property) const; void _get_property_list(List* p_list) const; static void _bind_methods(); private: template void _play(bool should_start_event); void set_space_attribute(const Ref& p_event) const; void _set_parameter_value(Parameter* parameter, const Variant& p_property); void _apply_parameters(const Ref& p_event); void _apply_parameters(); void free(); void _load_event_description_if_needed() const; void _load_event(); void _unload_event(); Ref _create_event(); Parameter* _find_parameter(const String& p_name) const; Parameter* _find_parameter(uint64_t p_id) const; void _stop_and_restart_if_autoplay(); Ref _get_parameter_description(const Parameter& parameter) const; static bool _should_load_by_event_name(); }; template void FmodEventEmitter::set_space_attribute(const Ref& p_event) const { static_cast(this)->set_space_attribute_impl(p_event); } template void FmodEventEmitter::free() { static_cast(this)->free_impl(); } template void FmodEventEmitter::ready() { #ifdef TOOLS_ENABLED // ensure we only run FMOD when the game is running! if (Engine::get_singleton()->is_editor_hint()) { return; } #endif if (_autoplay) { play(); } else if (_preload_event) { // No need to preload if autoplay is on because event is loaded anyway. _load_event(); } } template void FmodEventEmitter::process() { #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { return; } #endif if (_event.is_null()) { // No event loaded, nothing to do here return; } if (!_event->is_valid()) { // Event was loaded and is done playing. if (_auto_release) { free(); return; } if (_autoplay) { play(); } } if (_attached && _event->is_valid()) { set_space_attribute(_event); } } template void FmodEventEmitter::exit_tree() { #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { return; } #endif if (_event.is_null()) { // No event loaded, nothing to stop or free. return; } if (_event->is_valid()) { _event->release(); stop(); } } template void FmodEventEmitter::_notification(int p_what) { #ifdef TOOLS_ENABLED // ensure we only run FMOD when the game is running! if (Engine::get_singleton()->is_editor_hint()) { return; } #endif switch (p_what) { case Node::NOTIFICATION_PAUSED: set_paused(true); break; case Node::NOTIFICATION_UNPAUSED: if (is_paused()) { set_paused(false); } break; case Node::NOTIFICATION_READY: ready(); break; case Node::NOTIFICATION_PROCESS: process(); break; case Node::NOTIFICATION_EXIT_TREE: exit_tree(); break; default: break; } } template bool FmodEventEmitter::is_paused() { if (_event.is_null() || !_event->is_valid()) { return false; } return _event->get_paused(); } template void FmodEventEmitter::play(bool restart_if_playing) { _play(restart_if_playing); } template void FmodEventEmitter::stop() { if (_event.is_null() || !_event->is_valid()) { return; } if (_allow_fadeout) { _event->stop(FMOD_STUDIO_STOP_ALLOWFADEOUT); return; } _event->stop(FMOD_STUDIO_STOP_IMMEDIATE); } template void FmodEventEmitter::play_one_shot() { _play(true); } template template void FmodEventEmitter::_play(bool should_start_event) { Ref event; if (is_one_shot) { event = _create_event(); } else { if (_event.is_null() || !_event->is_valid()) { _load_event(); } event = _event; } if (event.is_null()) { // No event loaded, nothing to do here return; } event->set_volume(_volume); _apply_parameters(event); set_space_attribute(event); if (!_programmer_callback_sound_key.is_empty()) { event->set_programmer_callback(_programmer_callback_sound_key); } if (!should_start_event && event->get_playback_state() != FMOD_STUDIO_PLAYBACK_STOPPED) { return; } event->start(); event->release(); } template Variant FmodEventEmitter::get_parameter(const String& p_name) const { if (Parameter* parameter {_find_parameter(p_name)}) { return parameter->value; } return Variant(); } template void FmodEventEmitter::set_parameter(const String& p_name, const Variant& p_property) { Parameter* parameter {_find_parameter(p_name)}; if (!parameter) { Parameter param; _parameters.push_back(param); parameter = &_parameters[_parameters.size() - 1]; parameter->name = p_name; parameter->identifier.name = ¶meter->name; parameter->should_load_by_id = false; } _set_parameter_value(parameter, p_property); } template Variant FmodEventEmitter::get_parameter_by_id(uint64_t p_id) const { if (Parameter* parameter {_find_parameter(p_id)}) { return parameter->value; } return Variant(); } template void FmodEventEmitter::set_parameter_by_id(const uint64_t p_id, const Variant& p_property) { Parameter* parameter {_find_parameter(p_id)}; if (!parameter) { Parameter param; _parameters.push_back(param); parameter = &_parameters[_parameters.size() - 1]; parameter->id = p_id; parameter->should_load_by_id = true; parameter->identifier.id = p_id; } _set_parameter_value(parameter, p_property); } template void FmodEventEmitter::set_paused(bool p_is_paused) { if (_event.is_null() || !_event->is_valid()) { return; } _event->set_paused(p_is_paused); } template void FmodEventEmitter::_load_event_description_if_needed() const { if (!_event_description.is_null()) { return; } if (_should_load_by_event_name()) { #ifdef DEBUG_ENABLED if (FmodServer::get_singleton()->check_event_path(_event_name)) { #endif _event_description = FmodServer::get_singleton()->get_event(_event_name); #ifdef DEBUG_ENABLED } else { GODOT_LOG_ERROR(vformat("Cannot find event with path %s, will try with guid", _event_name)); GODOT_LOG_ERROR("You should fix this before releasing your game, check event exists and fallback is " "only a debug feature"); if (FmodServer::get_singleton()->check_event_guid_internal(_event_guid)) { _event_description = FmodServer::get_singleton()->get_event_from_guid_internal(_event_guid); } else { GODOT_LOG_ERROR( vformat("Cannot find event with guid %s and path %s. Please set right data from editor.", get_event_guid(), _event_name) ); GODOT_LOG_ERROR("You should fix this before releasing your game, check event exists and fallback " "is only a debug feature"); } } #endif } else { #ifdef DEBUG_ENABLED if (FmodServer::get_singleton()->check_event_guid_internal(_event_guid)) { #endif _event_description = FmodServer::get_singleton()->get_event_from_guid_internal(_event_guid); #ifdef DEBUG_ENABLED } else { GODOT_LOG_ERROR(vformat("Cannot find event with guid %s, will try with guid", get_event_guid())); GODOT_LOG_ERROR("You should fix this before releasing your game, check event exists and fallback is " "only a debug feature"); if (FmodServer::get_singleton()->check_event_path(_event_name)) { _event_description = FmodServer::get_singleton()->get_event(_event_name); } else { GODOT_LOG_ERROR( vformat("Cannot find event with guid %s and path %s. Please set right data from editor.", get_event_guid(), _event_name) ); GODOT_LOG_ERROR("You should fix this before releasing your game, check event exists and fallback " "is only a debug feature"); } } #endif } } template void FmodEventEmitter::_load_event() { _event = _create_event(); if (_event.is_null()) { return; } _event->set_callback(Callable(this, "_emit_callbacks"), FMOD_STUDIO_EVENT_CALLBACK_ALL); } template void FmodEventEmitter::_unload_event() { _event_description = Ref(); _event = Ref(); } template Ref FmodEventEmitter::_create_event() { _load_event_description_if_needed(); if (_event_description.is_null()) { return {}; } return FmodServer::get_singleton()->create_event_instance_from_description(_event_description); } template void FmodEventEmitter::_set_parameter_value(FmodEventEmitter::Parameter* parameter, const Variant& p_property) { parameter->value = p_property; if (_event.is_null() || !_event->is_valid()) { return; } #ifdef TOOLS_ENABLED if (!Engine::get_singleton()->is_editor_hint()) { #endif _apply_parameters(); #ifdef TOOLS_ENABLED } #endif } template void FmodEventEmitter::_apply_parameters(const Ref& p_event) { if (p_event.is_null() || !p_event->is_valid()) { return; } FmodServer::get_singleton()->apply_parameter_list_to_event(p_event, _parameters); } template void FmodEventEmitter::_apply_parameters() { _apply_parameters(_event); } template const Ref& FmodEventEmitter::get_event() const { return _event; } template void FmodEventEmitter::set_event_name(const String& name) { FMOD_GUID event_guid {FmodServer::get_singleton()->get_event_guid_internal(name)}; if (equals(_event_guid, event_guid)) { return; } _event_name = name; _event_guid = event_guid; _stop_and_restart_if_autoplay(); } template String FmodEventEmitter::get_event_name() const { return _event_name; } template void FmodEventEmitter::set_event_guid(const String& guid) { FMOD_GUID event_guid {string_to_fmod_guid(guid.utf8().get_data())}; if (equals(_event_guid, event_guid)) { return; } _event_guid = event_guid; _event_name = FmodServer::get_singleton()->get_event_path_internal(_event_guid); _stop_and_restart_if_autoplay(); } template String FmodEventEmitter::get_event_guid() const { return fmod_guid_to_string(_event_guid); } template void FmodEventEmitter::_stop_and_restart_if_autoplay() { if (!reinterpret_cast(this)->is_node_ready()) return; stop(); _unload_event(); if (_preload_event) { _load_event(); } _parameters.clear(); if (!_autoplay) return; play(); } template void FmodEventEmitter::set_attached(const bool attached) { _attached = attached; } template bool FmodEventEmitter::is_attached() const { return _attached; } template void FmodEventEmitter::set_autoplay(const bool autoplay) { _autoplay = autoplay; } template bool FmodEventEmitter::is_autoplay() const { return _autoplay; } template void FmodEventEmitter::set_auto_release(const bool auto_release) { _auto_release = auto_release; } template bool FmodEventEmitter::is_auto_release() const { return _auto_release; } template void FmodEventEmitter::set_allow_fadeout(const bool allow_fadeout) { _allow_fadeout = allow_fadeout; } template bool FmodEventEmitter::is_allow_fadeout() const { return _allow_fadeout; } template void FmodEventEmitter::set_preload_event(const bool preload_event) { _preload_event = preload_event; } template bool FmodEventEmitter::is_preload_event() const { return _preload_event; } template void FmodEventEmitter::set_volume(const float volume) { _volume = volume; if (_event.is_null() || !_event->is_valid()) { return; } _event->set_volume(volume); } template float FmodEventEmitter::get_volume() const { return _volume; } template void FmodEventEmitter::set_programmer_callback(const String &p_programmers_callback_sound_key) { _programmer_callback_sound_key = p_programmers_callback_sound_key; } template void FmodEventEmitter::_emit_callbacks(const Dictionary& dict, const int type) const { switch (type) { case FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT: const_cast(static_cast(this))->emit_signal(BEAT_SIGNAL_STRING, dict); break; case FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER: const_cast(static_cast(this))->emit_signal(MARKER_SIGNAL_STRING, dict); break; case FMOD_STUDIO_EVENT_CALLBACK_START_FAILED: const_cast(static_cast(this))->emit_signal(START_FAILED_SIGNAL_STRING); break; case FMOD_STUDIO_EVENT_CALLBACK_STARTED: const_cast(static_cast(this))->emit_signal(STARTED_SIGNAL_STRING); break; case FMOD_STUDIO_EVENT_CALLBACK_RESTARTED: const_cast(static_cast(this))->emit_signal(RESTARTED_SIGNAL_STRING); break; case FMOD_STUDIO_EVENT_CALLBACK_STOPPED: const_cast(static_cast(this))->emit_signal(STOPPED_SIGNAL_STRING); break; } } template typename FmodEventEmitter::Parameter* FmodEventEmitter::_find_parameter(const String& p_name ) const { Parameter* parameter {nullptr}; for (const Parameter& item : _parameters) { if (item.name != p_name) { continue; } parameter = const_cast(&item); break; } return parameter; } template typename FmodEventEmitter::Parameter* FmodEventEmitter::_find_parameter(uint64_t p_id) const { Parameter* parameter {nullptr}; for (const Parameter& item : _parameters) { if (item.id != p_id) { continue; } parameter = const_cast(&item); break; } return parameter; } template bool FmodEventEmitter::_should_load_by_event_name() { #ifndef TOOLS_ENABLED static #endif bool should_load_by_name {ProjectSettings::get_singleton()->get_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::SHOULD_LOAD_BY_NAME) )}; return should_load_by_name; } template bool FmodEventEmitter::_set(const StringName& p_name, const Variant& p_property) { if (!p_name.begins_with(FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)) { return false; } if (p_name == FmodStringNames::get_instance()->event_parameter_prefix_for_properties) { return false; } PackedStringArray parts {p_name.trim_prefix(vformat("%s/", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)).split("/")}; const String& parameter_name {parts[0]}; Parameter* parameter {_find_parameter(parameter_name)}; if (!parameter) { Parameter param; _parameters.push_back(param); parameter = &_parameters[_parameters.size() - 1]; parameter->name = parameter_name; parameter->should_load_by_id = !_should_load_by_event_name(); } if (parts.size() == 1) { _set_parameter_value(parameter, p_property); return true; } const String& parameter_end = parts[1]; if (parameter_end == "id") { uint64_t parameter_id = p_property.operator uint64_t(); parameter->id = parameter_id; if (parameter->should_load_by_id) { parameter->identifier.id = parameter_id; return true; } parameter->identifier.name = ¶meter->name; return true; } if (parameter_end == "variant_type") { parameter->variant_type = static_cast(p_property.operator int32_t()); return true; } if (parameter_end == "labels") { parameter->labels = p_property; return true; } return false; } template bool FmodEventEmitter::_get(const StringName& p_name, Variant& r_property) const { if (!p_name.begins_with(FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)) { return false; } if (p_name == FmodStringNames::get_instance()->event_parameter_prefix_for_properties) { return false; } PackedStringArray parts {p_name.trim_prefix(vformat("%s/", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)).split("/")}; Parameter* parameter {_find_parameter(parts[0])}; if (!parameter) { return false; } if (parts.size() == 1) { r_property = parameter->value; return true; } const String& parameter_end = parts[1]; if (parameter_end == "id") { r_property = parameter->id; return true; } if (parameter_end == "variant_type") { r_property = parameter->variant_type; return true; } if (parameter_end == "labels") { r_property = parameter->labels; return true; } return false; } template bool FmodEventEmitter::_property_can_revert(const StringName& p_name) const { if (!p_name.begins_with(FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)) { return false; } if (p_name == FmodStringNames::get_instance()->event_parameter_prefix_for_properties) { return false; } PackedStringArray parts {p_name.trim_prefix(vformat("%s/", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)).split("/")}; if (parts.size() == 1) { return true; } return false; } template bool FmodEventEmitter::_property_get_revert(const StringName& p_name, Variant& r_property) const { if (!p_name.begins_with(FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)) { return false; } if (p_name == FmodStringNames::get_instance()->event_parameter_prefix_for_properties) { return false; } PackedStringArray parts {p_name.trim_prefix(vformat("%s/", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES)).split("/")}; Parameter* parameter {_find_parameter(parts[0])}; if (!parameter) { return false; } if (parts.size() == 1) { Ref desc {_get_parameter_description(*parameter)}; if (desc.is_null()) { return false; } r_property = desc->get_default_value(); return true; } return false; } template void FmodEventEmitter::_get_property_list(List* p_list) const { p_list->push_back( PropertyInfo( Variant::Type::DICTIONARY, FmodStringNames::get_instance()->event_parameter_prefix_for_properties, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR ) ); for (const Parameter& parameter : _parameters) { const String& parameter_name {parameter.name}; Ref parameter_description{_get_parameter_description(parameter) }; if (parameter_description.is_null()) { // Skip parameters that cannot be resolved (e.g., missing or stale IDs) continue; } const float parameter_min_value {parameter_description->get_minimum()}; const float parameter_max_value {parameter_description->get_maximum()}; const Variant::Type parameter_variant_type {parameter.variant_type}; p_list->push_back( PropertyInfo(Variant::Type::INT, vformat("%s/%s/id", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR) ); if (!parameter.labels.is_empty()) { p_list->push_back( PropertyInfo( parameter_variant_type, vformat("%s/%s", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name), PROPERTY_HINT_ENUM, vformat(String(",").join(parameter.labels)) ) ); p_list->push_back( PropertyInfo( Variant::Type::PACKED_STRING_ARRAY, vformat("%s/%s/labels", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR ) ); } else { p_list->push_back( PropertyInfo( parameter_variant_type, vformat("%s/%s", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name), PROPERTY_HINT_RANGE, vformat("%s,%s,0.1", parameter_min_value, parameter_max_value) ) ); } p_list->push_back( PropertyInfo( Variant::Type::INT, vformat("%s/%s/variant_type", FmodStringNames::EVENT_PARAMETER_PREFIX_FOR_PROPERTIES, parameter_name), PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_NO_EDITOR ) ); } } template Ref FmodEventEmitter::_get_parameter_description(const FmodEventEmitter::Parameter& parameter) const { _load_event_description_if_needed(); return parameter.should_load_by_id ? _event_description->get_parameter_by_id(parameter.id) : _event_description->get_parameter_by_name(parameter.name); } #ifdef TOOLS_ENABLED template void FmodEventEmitter::tool_remove_all_parameters() { if (!Engine::get_singleton()->is_editor_hint()) { return; } _parameters.clear(); } template void FmodEventEmitter::tool_remove_parameter(uint64_t parameter_id) { if (!Engine::get_singleton()->is_editor_hint()) { return; } for (int i = _parameters.size() - 1; i >= 0; --i) { const Parameter& parameter {_parameters[i]}; if (parameter.id != parameter_id) continue; _parameters.erase(parameter); } } #endif template const StringName& FmodEventEmitter::get_class_static() { return Derived::get_class_static(); } template void FmodEventEmitter::_bind_methods() { ClassDB::bind_method(D_METHOD("play", "restart_if_playing"), &Derived::play, DEFVAL(true)); ClassDB::bind_method(D_METHOD("play_one_shot"), &Derived::play_one_shot); ClassDB::bind_method(D_METHOD("stop"), &Derived::stop); ClassDB::bind_method(D_METHOD("set_parameter", "name", "value"), &Derived::set_parameter); ClassDB::bind_method(D_METHOD("get_parameter", "name"), &Derived::get_parameter); ClassDB::bind_method(D_METHOD("set_parameter_by_id", "id", "value"), &Derived::set_parameter_by_id); ClassDB::bind_method(D_METHOD("get_parameter_by_id", "id"), &Derived::get_parameter_by_id); ClassDB::bind_method(D_METHOD("is_paused"), &Derived::is_paused); ClassDB::bind_method(D_METHOD("set_paused", "p_is_paused"), &Derived::set_paused); ClassDB::bind_method(D_METHOD("set_event_name", "event_name"), &Derived::set_event_name); ClassDB::bind_method(D_METHOD("get_event_name"), &Derived::get_event_name); ClassDB::bind_method(D_METHOD("set_event_guid", "event_guid"), &Derived::set_event_guid); ClassDB::bind_method(D_METHOD("get_event_guid"), &Derived::get_event_guid); ClassDB::bind_method(D_METHOD("set_attached", "attached"), &Derived::set_attached); ClassDB::bind_method(D_METHOD("is_attached"), &Derived::is_attached); ClassDB::bind_method(D_METHOD("set_autoplay", "_autoplay"), &Derived::set_autoplay); ClassDB::bind_method(D_METHOD("is_autoplay"), &Derived::is_autoplay); ClassDB::bind_method(D_METHOD("set_auto_release", "_autoplay"), &Derived::set_auto_release); ClassDB::bind_method(D_METHOD("is_auto_release"), &Derived::is_auto_release); ClassDB::bind_method(D_METHOD("set_allow_fadeout", "allow_fadeout"), &Derived::set_allow_fadeout); ClassDB::bind_method(D_METHOD("is_allow_fadeout"), &Derived::is_allow_fadeout); ClassDB::bind_method(D_METHOD("set_preload_event", "preload_event"), &Derived::set_preload_event); ClassDB::bind_method(D_METHOD("is_preload_event"), &Derived::is_preload_event); ClassDB::bind_method(D_METHOD("get_volume"), &Derived::get_volume); ClassDB::bind_method(D_METHOD("set_volume", "p_volume"), &Derived::set_volume); ClassDB::bind_method(D_METHOD("set_programmer_callback", "p_programmers_callback_sound_key"), &Derived::set_programmer_callback); ClassDB::bind_method(D_METHOD("_emit_callbacks", "dict", "type"), &Derived::_emit_callbacks); #ifdef TOOLS_ENABLED ClassDB::bind_method(D_METHOD("tool_remove_all_parameters"), &Derived::tool_remove_all_parameters); ClassDB::bind_method(D_METHOD("tool_remove_parameter", "parameter_id"), &Derived::tool_remove_parameter); #endif ADD_PROPERTY(PropertyInfo(Variant::STRING, "event_name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_event_name", "get_event_name"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "event_guid", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_event_guid", "get_event_guid"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "attached", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_attached", "is_attached"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_autoplay", "is_autoplay"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_release", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_auto_release", "is_auto_release"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_fadeout", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_allow_fadeout", "is_allow_fadeout"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "preload_event", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_preload_event", "is_preload_event"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), "set_volume", "get_volume"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "paused", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_paused", "is_paused"); ADD_SIGNAL(MethodInfo(BEAT_SIGNAL_STRING, PropertyInfo(Variant::DICTIONARY, "params"))); ADD_SIGNAL(MethodInfo(MARKER_SIGNAL_STRING, PropertyInfo(Variant::DICTIONARY, "params"))); ADD_SIGNAL(MethodInfo(START_FAILED_SIGNAL_STRING)); ADD_SIGNAL(MethodInfo(STARTED_SIGNAL_STRING)); ADD_SIGNAL(MethodInfo(RESTARTED_SIGNAL_STRING)); ADD_SIGNAL(MethodInfo(STOPPED_SIGNAL_STRING)); } }// namespace godot #endif// GODOTFMOD_FMOD_EVENT_EMITTER_H ================================================ FILE: src/nodes/fmod_event_emitter_2d.cpp ================================================ #include using namespace godot; void FmodEventEmitter2D::set_space_attribute_impl(const Ref& p_event) const { p_event->set_2d_attributes(get_global_transform()); } void FmodEventEmitter2D::_ready() { FmodEventEmitter::_ready(); } void FmodEventEmitter2D::_process(double delta) { FmodEventEmitter::_process(delta); } void FmodEventEmitter2D::_notification(int p_what) { FmodEventEmitter::_notification(p_what); } void FmodEventEmitter2D::_exit_tree() { FmodEventEmitter::_exit_tree(); } void FmodEventEmitter2D::_bind_methods() { FmodEventEmitter::_bind_methods(); } void FmodEventEmitter2D::free_impl() { queue_free(); } ================================================ FILE: src/nodes/fmod_event_emitter_2d.h ================================================ #ifndef FMOD_EVENT_EMITTER_2D_GODOT_FMOD_H #define FMOD_EVENT_EMITTER_2D_GODOT_FMOD_H #include "fmod_event_emitter.h" #include "studio/fmod_event.h" #include namespace godot { class FmodEventEmitter2D : public FmodEventEmitter { friend class FmodEventEmitter; GDCLASS(FmodEventEmitter2D, Node2D) private: void set_space_attribute_impl(const Ref& p_event) const; void free_impl(); public: FmodEventEmitter2D() = default; ~FmodEventEmitter2D() override = default; virtual void _ready() override; virtual void _process(double delta) override; void _notification(int p_what); virtual void _exit_tree() override; protected: static void _bind_methods(); }; }// namespace godot #endif// FMOD_EVENT_EMITTER_2D_GODOT_FMOD_H ================================================ FILE: src/nodes/fmod_event_emitter_3d.cpp ================================================ #include using namespace godot; void FmodEventEmitter3D::set_space_attribute_impl(const Ref& p_event) const { p_event->set_3d_attributes(get_global_transform()); } void FmodEventEmitter3D::_ready() { FmodEventEmitter::_ready(); } void FmodEventEmitter3D::_process(double delta) { FmodEventEmitter::_process(delta); } void FmodEventEmitter3D::_notification(int p_what) { FmodEventEmitter::_notification(p_what); } void FmodEventEmitter3D::_exit_tree() { FmodEventEmitter::_exit_tree(); } void FmodEventEmitter3D::_bind_methods() { FmodEventEmitter::_bind_methods(); } void FmodEventEmitter3D::free_impl() { queue_free(); } ================================================ FILE: src/nodes/fmod_event_emitter_3d.h ================================================ #ifndef GODOTFMOD_FMOD_EVENT_EMITTER_3D_H #define GODOTFMOD_FMOD_EVENT_EMITTER_3D_H #include "classes/node.hpp" #include "classes/node3d.hpp" #include "fmod_event_emitter.h" namespace godot { class FmodEventEmitter3D : public FmodEventEmitter { friend class FmodEventEmitter; GDCLASS(FmodEventEmitter3D, Node3D) private: void set_space_attribute_impl(const Ref& p_event) const; void free_impl(); public: FmodEventEmitter3D() = default; ~FmodEventEmitter3D() override = default; virtual void _ready() override; virtual void _process(double delta) override; void _notification(int p_what); virtual void _exit_tree() override; static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_EVENT_EMITTER_3D_H ================================================ FILE: src/nodes/fmod_listener.h ================================================ #ifndef GODOTFMOD_FMOD_LISTENER_H #define GODOTFMOD_FMOD_LISTENER_H #include #include namespace godot { template class FmodListener : public NodeType { void ready(); void exit_tree(); public: void _notification(int p_what); void set_listener_index(const int index); int get_listener_index() const; void set_locked(const bool locked); bool get_locked() const; void set_listener_weight(const float p_weight); float get_listener_weight() const; static const StringName& get_class_static(); FmodListener(); ~FmodListener() = default; private: float _weight; int _listener_index; bool _is_locked; bool _is_added; protected: static void _bind_methods(); }; template void FmodListener::_notification(int p_what) { #ifdef TOOLS_ENABLED // ensure we only run FMOD when the game is running! if (Engine::get_singleton()->is_editor_hint()) { return; } #endif switch(p_what){ case Node::NOTIFICATION_READY: ready(); break; case Node::NOTIFICATION_EXIT_TREE: exit_tree(); break; default: break; } } template void FmodListener::ready() { #ifdef TOOLS_ENABLED // ensure we only run FMOD when the game is running! if (Engine::get_singleton()->is_editor_hint()) { return; } #endif FmodServer::get_singleton()->add_listener(_listener_index, this); FmodServer::get_singleton()->set_listener_lock(_listener_index, _is_locked); FmodServer::get_singleton()->set_system_listener_weight(_listener_index, _weight); _is_added = true; } template void FmodListener::exit_tree() { #ifdef TOOLS_ENABLED // ensure we only run FMOD when the game is running! if (Engine::get_singleton()->is_editor_hint()) { return; } #endif FmodServer::get_singleton()->remove_listener(_listener_index, this); _is_added = false; } template void FmodListener::set_listener_index(const int index) { _listener_index = index; } template int FmodListener::get_listener_index() const { return _listener_index; } template void FmodListener::set_locked(const bool locked) { _is_locked = locked; if (!_is_added) { return; } FmodServer::get_singleton()->set_listener_lock(_listener_index, locked); } template bool FmodListener::get_locked() const { if (!_is_added) { return _is_locked; } return FmodServer::get_singleton()->get_listener_lock(_listener_index); } template void FmodListener::set_listener_weight(const float p_weight) { _weight = p_weight; if (!_is_added) { return; } FmodServer::get_singleton()->set_system_listener_weight(_listener_index, p_weight); } template float FmodListener::get_listener_weight() const { if (!_is_added) { return _weight; } return FmodServer::get_singleton()->get_system_listener_weight(_listener_index); } template FmodListener::FmodListener() : NodeType(), _weight(1.0), _listener_index(0), _is_locked(false), _is_added(false) {} template void FmodListener::_bind_methods() { ClassDB::bind_method(D_METHOD("set_listener_index", "index"), &Derived::set_listener_index); ClassDB::bind_method(D_METHOD("get_listener_index"), &Derived::get_listener_index); ClassDB::bind_method(D_METHOD("set_locked", "locked"), &Derived::set_locked); ClassDB::bind_method(D_METHOD("get_locked"), &Derived::get_locked); ClassDB::bind_method(D_METHOD("set_listener_weight", "p_weight"), &Derived::set_listener_weight); ClassDB::bind_method(D_METHOD("get_listener_weight"), &Derived::get_listener_weight); ADD_PROPERTY( PropertyInfo( Variant::INT, "listener_index", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_listener_index", "get_listener_index" ); ADD_PROPERTY( PropertyInfo( Variant::BOOL, "is_locked", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_locked", "get_locked" ); ADD_PROPERTY( PropertyInfo( Variant::FLOAT, "weight", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_listener_weight", "get_listener_weight" ); } template const StringName& FmodListener::get_class_static() { return Derived::get_class_static(); } }// namespace godot #endif// GODOTFMOD_FMOD_LISTENER_H ================================================ FILE: src/nodes/fmod_listener_2d.cpp ================================================ #include "fmod_listener_2d.h" using namespace godot; void FmodListener2D::_bind_methods() { FmodListener::_bind_methods(); } void FmodListener2D::_ready() { FmodListener::_ready(); } void FmodListener2D::_exit_tree() { FmodListener::_exit_tree(); } ================================================ FILE: src/nodes/fmod_listener_2d.h ================================================ #ifndef GODOTFMOD_FMOD_LISTENER_2D_H #define GODOTFMOD_FMOD_LISTENER_2D_H #include "classes/node.hpp" #include "classes/node2d.hpp" #include "fmod_listener.h" namespace godot { class FmodListener2D : public FmodListener { GDCLASS(FmodListener2D, Node2D) public: virtual void _ready() override; virtual void _exit_tree() override; protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_LISTENER_2D_H ================================================ FILE: src/nodes/fmod_listener_3d.cpp ================================================ #include "fmod_listener_3d.h" #include "fmod_server.h" using namespace godot; void FmodListener3D::_bind_methods() { FmodListener::_bind_methods(); } void FmodListener3D::_ready() { FmodListener::_ready(); } void FmodListener3D::_exit_tree() { FmodListener::_exit_tree(); } ================================================ FILE: src/nodes/fmod_listener_3d.h ================================================ #ifndef GODOTFMOD_FMOD_LISTENER_3D_H #define GODOTFMOD_FMOD_LISTENER_3D_H #include "classes/node.hpp" #include "classes/node3d.hpp" #include "fmod_listener.h" namespace godot { class FmodListener3D : public FmodListener { GDCLASS(FmodListener3D, Node3D) public: virtual void _ready() override; virtual void _exit_tree() override; protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_LISTENER_3D_H ================================================ FILE: src/plugins/ios_plugins_loader.h ================================================ #ifndef GODOTFMOD_IOS_PLUGINS_LOADER_H #define GODOTFMOD_IOS_PLUGINS_LOADER_H #ifdef IOS_ENABLED typedef void* FMOD_SYSTEM_PTR; typedef uint32_t (*REGISTER_DSP_METHOD)(FMOD_SYSTEM_PTR system, FMOD_DSP_DESCRIPTION* description, uint32_t* handle); typedef uint32_t (*REGISTER_CODEC_METHOD)(FMOD_SYSTEM_PTR system, FMOD_CODEC_DESCRIPTION* description, uint32_t* handle); typedef uint32_t (*REGISTER_OUTPUT_METHOD)(FMOD_SYSTEM_PTR system, FMOD_OUTPUT_DESCRIPTION* description, uint32_t* handle); typedef struct { FMOD_SYSTEM_PTR system; REGISTER_DSP_METHOD register_dsp_method; REGISTER_CODEC_METHOD register_codec_method; REGISTER_OUTPUT_METHOD register_output_method; } FMOD_IOS_INTERFACE; extern "C" { uint32_t* load_all_fmod_plugins(FMOD_IOS_INTERFACE* ios_interface, uint32_t* r_count); }; #endif #endif //GODOTFMOD_IOS_PLUGINS_LOADER_H ================================================ FILE: src/plugins/plugins_helper.h ================================================ #ifndef GODOTFMOD_PLUGINS_HELPER_H #define GODOTFMOD_PLUGINS_HELPER_H #include #include #include namespace godot { static String get_fmod_plugins_base_path(const Ref& p_settings) { #ifdef TOOLS_ENABLED return p_settings->get_plugins_base_path(); #else return OS::get_singleton() ->get_executable_path() .get_base_dir() #ifdef MACOS_ENABLED .path_join("../PlugIns/") #endif ; #endif } #if !defined(ANDROID_ENABLED) || defined(TOOLS_ENABLED) static String get_plugins_os_directory(const Ref& p_settings, const String& p_os_lower_name, const String& p_arch) { String plugin_directory = get_fmod_plugins_base_path(p_settings); #ifdef TOOLS_ENABLED plugin_directory = plugin_directory.path_join(p_os_lower_name); if (!p_arch.is_empty()) { plugin_directory = plugin_directory.path_join(p_arch); } #endif return plugin_directory; } #endif static Vector get_fmod_plugins_libraries_paths(const Ref& p_settings, const String& os_override = "", const String& arch = "") { Vector result; #if !defined(ANDROID_ENABLED) || defined(TOOLS_ENABLED) String os_lower_name = os_override.is_empty() ? OS::get_singleton()->get_name().to_lower() : os_override.to_lower(); String plugin_directory = get_plugins_os_directory(p_settings, os_lower_name, arch); String plugin_extension; String plugin_lib_prefix = "lib"; if (os_lower_name == "windows") { plugin_extension = "dll"; plugin_lib_prefix = ""; } else if (os_lower_name == "macos") { plugin_extension = "dylib"; } else { plugin_extension = "so"; } PackedStringArray plugin_list = p_settings->get_dynamic_plugin_list(); for (const String& plugin : plugin_list) { result.append(plugin_directory.path_join(vformat("%s%s.%s", plugin_lib_prefix, plugin, plugin_extension))); } #else for (const String& plugin : p_settings->get_dynamic_plugin_list()) { result.append(vformat("lib%s.so", plugin)); } #endif return result; } } #endif //GODOTFMOD_PLUGINS_HELPER_H ================================================ FILE: src/register_types.cpp ================================================ #include "constants.h" #include "core/fmod_sound.h" #include "data/performance_data.h" #include "fmod_server.h" #include "nodes/fmod_bank_loader.h" #include "nodes/fmod_event_emitter_2d.h" #include "nodes/fmod_event_emitter_3d.h" #include "nodes/fmod_listener_2d.h" #include "nodes/fmod_listener_3d.h" #include "studio/fmod_bank.h" #include "studio/fmod_bus.h" #include "studio/fmod_event.h" #include "studio/fmod_event_description.h" #include "studio/fmod_vca.h" #include "fmod_string_names.h" #include "resources/fmod_logging_settings.h" #ifdef TOOLS_ENABLED #include #endif #include #include #include #include #include #include #include using namespace godot; static FmodServer* fmod_singleton; void initialize_fmod_with_settings() { Ref general_settings = FmodGeneralSettings::get_from_project_settings(); Ref software_format_settings = FmodSoftwareFormatSettings::get_from_project_settings(); Ref dsp_settings = FmodDspSettings::get_from_project_settings(); Ref three_d_settings = FmodSound3DSettings::get_from_project_settings(); FmodServer::get_singleton()->set_software_format(software_format_settings); FmodServer::get_singleton()->set_system_dsp_buffer_size(dsp_settings); FmodServer::get_singleton()->init(general_settings); FmodServer::get_singleton()->set_sound_3d_settings(three_d_settings); FmodServer::get_singleton()->set_system_listener_number(general_settings->get_default_listener_count()); FmodServer::get_singleton()->load_all_plugins(FmodPluginsSettings::get_from_project_settings()); } void initialize_fmod() { #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { initialize_fmod_with_settings(); return; } #endif bool auto_initialize = ProjectSettings::get_singleton()->get_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FMOD_SETTING_AUTO_INITIALIZE), DEFAULT_AUTO_INITIALIZE ); if (auto_initialize) { initialize_fmod_with_settings(); } } void initialize_fmod_module(ModuleInitializationLevel p_level) { if (p_level == MODULE_INITIALIZATION_LEVEL_CORE) { // initialise filerunner singleton by calling it. FmodStringNames::create(); Callbacks::GodotFileRunner::get_singleton(); } if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { // Data ClassDB::register_class(); // Core ClassDB::register_class(); ClassDB::register_class(); // Studio ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); // Nodes ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); // Resources ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); // Server ClassDB::register_class(); fmod_singleton = memnew(FmodServer); Engine::get_singleton()->register_singleton("FmodServer", FmodServer::get_singleton()); initialize_fmod(); } #ifdef TOOLS_ENABLED if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { ClassDB::register_class(); ClassDB::register_class(); EditorPlugins::add_by_type(); } #endif } void uninitialize_fmod_module(ModuleInitializationLevel p_level) { if (p_level == MODULE_INITIALIZATION_LEVEL_CORE) { Callbacks::GodotFileRunner::get_singleton()->finish(); FmodStringNames::free(); } if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { fmod_singleton->shutdown(); Engine::get_singleton()->unregister_singleton("FmodServer"); memdelete(fmod_singleton); } } extern "C" { // Initialization. GDExtensionBool GDE_EXPORT fmod_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_fmod_module); init_obj.register_terminator(uninitialize_fmod_module); init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_CORE); return init_obj.init(); } } ================================================ FILE: src/register_types.h ================================================ #ifndef FMOD_REGISTER_TYPES_H #define FMOD_REGISTER_TYPES_H #include using namespace godot; void initialize_fmod_module(ModuleInitializationLevel p_level); void uninitialize_fmod_module(ModuleInitializationLevel p_level); #endif// ! FMOD_REGISTER_TYPES_H ================================================ FILE: src/resources/fmod_dsp_settings.cpp ================================================ #include "fmod_dsp_settings.h" #include #include using namespace godot; void FmodDspSettings::set_dsp_buffer_size(const unsigned int p_dsp_buffer_size) { _dsp_buffer_size = p_dsp_buffer_size; } unsigned int FmodDspSettings::get_dsp_buffer_size() const { return _dsp_buffer_size; } void FmodDspSettings::set_dsp_buffer_count(const int p_dsp_buffer_count) { _dsp_buffer_count = p_dsp_buffer_count; } int FmodDspSettings::get_dsp_buffer_count() const { return _dsp_buffer_count; } Ref FmodDspSettings::get_from_project_settings() { Ref settings; settings.instantiate(); ProjectSettings* project_settings = ProjectSettings::get_singleton(); String dsp_buffer_size_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, DSP_SETTINGS_BASE_PATH, DSP_BUFFER_SIZE_OPTION); settings->set_dsp_buffer_size(project_settings->get_setting(dsp_buffer_size_setting_path, DEFAULT_DSP_BUFFER_SIZE)); String dsp_buffer_count_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, DSP_SETTINGS_BASE_PATH, DSP_BUFFER_COUNT_OPTION); settings->set_dsp_buffer_count(project_settings->get_setting(dsp_buffer_count_setting_path, DEFAULT_DSP_BUFFER_COUNT)); return settings; } void FmodDspSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_dsp_buffer_size", "p_dsp_buffer_size"), &FmodDspSettings::set_dsp_buffer_size); ClassDB::bind_method(D_METHOD("get_dsp_buffer_size"), &FmodDspSettings::get_dsp_buffer_size); ClassDB::bind_method(D_METHOD("set_dsp_buffer_count", "p_dsp_buffer_count"), &FmodDspSettings::set_dsp_buffer_count); ClassDB::bind_method(D_METHOD("get_dsp_buffer_count"), &FmodDspSettings::get_dsp_buffer_count); ADD_PROPERTY( PropertyInfo( Variant::INT, "dsp_buffer_size", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_dsp_buffer_size", "get_dsp_buffer_size" ); ADD_PROPERTY( PropertyInfo( Variant::INT, "dsp_buffer_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_dsp_buffer_count", "get_dsp_buffer_count" ); } ================================================ FILE: src/resources/fmod_dsp_settings.h ================================================ #ifndef GODOTFMOD_FMOD_DSP_SETTINGS_H #define GODOTFMOD_FMOD_DSP_SETTINGS_H #include namespace godot { class FmodDspSettings : public Resource { GDCLASS(FmodDspSettings, Resource) public: void set_dsp_buffer_size(const unsigned int p_dsp_buffer_size); unsigned int get_dsp_buffer_size() const; void set_dsp_buffer_count(const int p_dsp_buffer_count); int get_dsp_buffer_count() const; static Ref get_from_project_settings(); FmodDspSettings() = default; ~FmodDspSettings() = default; static constexpr const char* DSP_SETTINGS_BASE_PATH = "DSP"; static constexpr const char* DSP_BUFFER_SIZE_OPTION = "dsp_buffer_size"; static constexpr const char* DSP_BUFFER_COUNT_OPTION = "dsp_buffer_count"; static constexpr const int DEFAULT_DSP_BUFFER_SIZE = 512; static constexpr const int DEFAULT_DSP_BUFFER_COUNT = 4; private: unsigned int _dsp_buffer_size; int _dsp_buffer_count; protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_DSP_SETTINGS_H ================================================ FILE: src/resources/fmod_logging_settings.cpp ================================================ #include "fmod_logging_settings.h" #include #include #include using namespace godot; void FmodLoggingSettings::set_debug_level(int p_debug_level) { _debug_level = p_debug_level; } int FmodLoggingSettings::get_debug_level() const { return _debug_level; } int FmodLoggingSettings::_debug_level_to_fmod() const { switch (_debug_level) { case DEBUG_INHERIT: if (OS::get_singleton()->is_stdout_verbose()) { return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING | FMOD_DEBUG_LEVEL_LOG | FMOD_DEBUG_TYPE_TRACE; } else { return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING; } case DEBUG_NONE: return FMOD_DEBUG_LEVEL_NONE; case DEBUG_ERROR: return FMOD_DEBUG_LEVEL_ERROR; case DEBUG_WARNING: return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING; case DEBUG_LOG: return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING | FMOD_DEBUG_LEVEL_LOG; case DEBUG_VERBOSE: return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING | FMOD_DEBUG_LEVEL_LOG | FMOD_DEBUG_TYPE_TRACE; default: return FMOD_DEBUG_LEVEL_ERROR | FMOD_DEBUG_LEVEL_WARNING; } } void FmodLoggingSettings::set_log_output(int p_log_output) { _log_output = p_log_output; } int FmodLoggingSettings::get_log_output() const { return _log_output; } void FmodLoggingSettings::set_log_file_path(const String& p_log_file_path) { _log_file_path = p_log_file_path; } const String& FmodLoggingSettings::get_log_file_path() const { return _log_file_path; } Ref FmodLoggingSettings::get_from_project_settings() { Ref settings; settings.instantiate(); ProjectSettings* project_settings = ProjectSettings::get_singleton(); String debug_level_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, LOGGING_SETTINGS_BASE_PATH, DEBUG_LEVEL_OPTION); settings->set_debug_level(project_settings->get_setting(debug_level_setting_path, DEFAULT_DEBUG_LEVEL)); String log_output_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, LOGGING_SETTINGS_BASE_PATH, LOG_OUTPUT_OPTION); settings->set_log_output(project_settings->get_setting(log_output_setting_path, DEFAULT_LOG_OUTPUT)); String log_file_path_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, LOGGING_SETTINGS_BASE_PATH, LOG_FILE_PATH_OPTION); settings->set_log_file_path(project_settings->get_setting(log_file_path_setting_path, DEFAULT_LOG_FILE_PATH)); return settings; } void FmodLoggingSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_debug_level", "debug_level"), &FmodLoggingSettings::set_debug_level); ClassDB::bind_method(D_METHOD("get_debug_level"), &FmodLoggingSettings::get_debug_level); ClassDB::bind_method(D_METHOD("set_log_output", "log_output"), &FmodLoggingSettings::set_log_output); ClassDB::bind_method(D_METHOD("get_log_output"), &FmodLoggingSettings::get_log_output); ClassDB::bind_method(D_METHOD("set_log_file_path", "log_file_path"), &FmodLoggingSettings::set_log_file_path); ClassDB::bind_method(D_METHOD("get_log_file_path"), &FmodLoggingSettings::get_log_file_path); // Debug level property ADD_PROPERTY( PropertyInfo(Variant::INT, "debug_level", PROPERTY_HINT_ENUM, "None,Error Only,Error and Warning,Full Log,Verbose"), "set_debug_level", "get_debug_level" ); // Log output property ADD_PROPERTY(PropertyInfo(Variant::INT, "log_output", PROPERTY_HINT_ENUM, "TTY,Godot,File"), "set_log_output", "get_log_output"); // Log file path property ADD_PROPERTY(PropertyInfo(Variant::STRING, "log_file_path", PROPERTY_HINT_FILE, "*.txt,*.log", PROPERTY_USAGE_DEFAULT), "set_log_file_path", "get_log_file_path"); // Bind enum constants BIND_ENUM_CONSTANT(DEBUG_NONE); BIND_ENUM_CONSTANT(DEBUG_ERROR); BIND_ENUM_CONSTANT(DEBUG_WARNING); BIND_ENUM_CONSTANT(DEBUG_LOG); BIND_ENUM_CONSTANT(DEBUG_VERBOSE); BIND_ENUM_CONSTANT(FMOD_DEBUG_MODE_TTY); BIND_ENUM_CONSTANT(FMOD_DEBUG_MODE_CALLBACK); BIND_ENUM_CONSTANT(FMOD_DEBUG_MODE_FILE); } ================================================ FILE: src/resources/fmod_logging_settings.h ================================================ #ifndef GODOTFMOD_FMOD_LOGGING_SETTINGS_H #define GODOTFMOD_FMOD_LOGGING_SETTINGS_H #include "fmod_studio.h" #include namespace godot { class FmodLoggingSettings : public Resource { GDCLASS(FmodLoggingSettings, Resource) public: enum DebugLevel { DEBUG_INHERIT, DEBUG_NONE, DEBUG_ERROR, DEBUG_WARNING, DEBUG_LOG, DEBUG_VERBOSE, }; void set_debug_level(int p_debug_level); int get_debug_level() const; int _debug_level_to_fmod() const; void set_log_output(int p_log_output); int get_log_output() const; void set_log_file_path(const String& p_log_file_path); const String& get_log_file_path() const; static Ref get_from_project_settings(); static constexpr const char* LOGGING_SETTINGS_BASE_PATH = "Logging"; // Setting keys static constexpr const char* DEBUG_LEVEL_OPTION = "debug_level"; static constexpr const char* LOG_OUTPUT_OPTION = "log_output"; static constexpr const char* LOG_FILE_PATH_OPTION = "log_file_path"; // Default values static constexpr const int DEFAULT_DEBUG_LEVEL = DEBUG_INHERIT; static constexpr const int DEFAULT_LOG_OUTPUT = FMOD_DEBUG_MODE_CALLBACK; static constexpr const char* DEFAULT_LOG_FILE_PATH = "user://fmod.log"; private: int _debug_level; int _log_output; String _log_file_path; protected: static void _bind_methods(); }; }// namespace godot VARIANT_ENUM_CAST(FmodLoggingSettings::DebugLevel); VARIANT_ENUM_CAST(FMOD_DEBUG_MODE); #endif// GODOTFMOD_FMOD_LOGGING_SETTINGS_H ================================================ FILE: src/resources/fmod_plugins_settings.cpp ================================================ #include "fmod_plugins_settings.h" #include "constants.h" #include #include #include #include using namespace godot; void FmodPluginsSettings::set_plugins_base_path(const String& p_base_path) { _plugins_base_path = p_base_path; } const String& FmodPluginsSettings::get_plugins_base_path() const { return _plugins_base_path; } void FmodPluginsSettings::set_dynamic_plugin_list(const PackedStringArray& p_dynamic_plugin_list) { _dynamic_plugin_list = p_dynamic_plugin_list; } const PackedStringArray& FmodPluginsSettings::get_dynamic_plugin_list() const { return _dynamic_plugin_list; } Ref FmodPluginsSettings::get_from_project_settings() { ProjectSettings* project_settings = ProjectSettings::get_singleton(); String resource_path = project_settings->get_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, PLUGINS_SETTINGS_BASE_PATH, RESOURCE_OPTION), FmodPluginsSettings::DEFAULT_RESOURCE_OPTION ); if (resource_path.is_empty()) { Ref settings; settings.instantiate(); return settings; } if (!FileAccess::file_exists(resource_path)) { GODOT_LOG_WARNING(vformat("Cannot find FmodPluginsSettings at %s", resource_path)); Ref settings; settings.instantiate(); return settings; } Ref settings = ResourceLoader::get_singleton()->load(resource_path); return settings; } void FmodPluginsSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_plugins_base_path", "p_base_path"), &FmodPluginsSettings::set_plugins_base_path); ClassDB::bind_method(D_METHOD("get_fmod_plugins_base_path"), &FmodPluginsSettings::get_plugins_base_path); ClassDB::bind_method(D_METHOD("set_dynamic_plugin_list", "p_dynamic_plugin_list"), &FmodPluginsSettings::set_dynamic_plugin_list); ClassDB::bind_method(D_METHOD("get_dynamic_plugin_list"), &FmodPluginsSettings::get_dynamic_plugin_list); ClassDB::bind_method(D_METHOD("set_static_plugins_methods", "p_static_plugins_settings"), &FmodPluginsSettings::set_static_plugins_methods); ClassDB::bind_method(D_METHOD("get_static_plugins_methods"), &FmodPluginsSettings::get_static_plugins_methods); ADD_PROPERTY( PropertyInfo( Variant::STRING, "plugins_base_path", PROPERTY_HINT_DIR, "", PROPERTY_USAGE_DEFAULT ), "set_plugins_base_path", "get_fmod_plugins_base_path" ); ADD_PROPERTY( PropertyInfo( Variant::PACKED_STRING_ARRAY, "dynamic_plugin_list", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_dynamic_plugin_list", "get_dynamic_plugin_list" ); ADD_PROPERTY( PropertyInfo( Variant::ARRAY, "static_plugins_methods", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "FmodStaticPluginMethod"), PROPERTY_USAGE_DEFAULT ), "set_static_plugins_methods", "get_static_plugins_methods" ); } void FmodPluginsSettings::set_static_plugins_methods(const Array& p_static_plugins_settings) { _static_plugins_methods = p_static_plugins_settings; } const Array& FmodPluginsSettings::get_static_plugins_methods() const { return _static_plugins_methods; } void FmodStaticPluginMethod::set_type(FmodStaticPluginMethod::Type p_type) { _type = p_type; } FmodStaticPluginMethod::Type FmodStaticPluginMethod::get_type() const { return _type; } void FmodStaticPluginMethod::set_method_name(const String& p_method_name) { _method_name = p_method_name; } const String& FmodStaticPluginMethod::get_method_name() const { return _method_name; } void FmodStaticPluginMethod::_bind_methods() { ClassDB::bind_method(D_METHOD("set_type", "p_type"), &FmodStaticPluginMethod::set_type); ClassDB::bind_method(D_METHOD("get_type"), &FmodStaticPluginMethod::get_type); ClassDB::bind_method(D_METHOD("set_method_name", "p_method_name"), &FmodStaticPluginMethod::set_method_name); ClassDB::bind_method(D_METHOD("get_method_name"), &FmodStaticPluginMethod::get_method_name); ADD_PROPERTY( PropertyInfo( Variant::INT, "type", PROPERTY_HINT_ENUM, "CODEC,DSP,OUTPUT", PROPERTY_USAGE_DEFAULT ), "set_type", "get_type" ); ADD_PROPERTY( PropertyInfo( Variant::STRING, "method_name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_method_name", "get_method_name" ); BIND_ENUM_CONSTANT(CODEC); BIND_ENUM_CONSTANT(DSP); BIND_ENUM_CONSTANT(OUTPUT); } ================================================ FILE: src/resources/fmod_plugins_settings.h ================================================ #ifndef GODOTFMOD_FMOD_PLUGINS_SETTINGS_H #define GODOTFMOD_FMOD_PLUGINS_SETTINGS_H #include namespace godot { class FmodStaticPluginMethod : public Resource { GDCLASS(FmodStaticPluginMethod, Resource) public: enum Type { CODEC, DSP, OUTPUT, COUNT }; void set_type(Type p_type); Type get_type() const; void set_method_name(const String& p_method_name); const String& get_method_name() const; private: Type _type = Type::CODEC; String _method_name; protected: static void _bind_methods(); }; class FmodPluginsSettings : public Resource { GDCLASS(FmodPluginsSettings, Resource) public: void set_plugins_base_path(const String& p_base_path); const String& get_plugins_base_path() const; void set_dynamic_plugin_list(const PackedStringArray& p_dynamic_plugin_list); const PackedStringArray& get_dynamic_plugin_list() const; void set_static_plugins_methods(const Array& p_static_plugins_settings); const Array& get_static_plugins_methods() const; static Ref get_from_project_settings(); FmodPluginsSettings() = default; ~FmodPluginsSettings() = default; static constexpr const char* PLUGINS_SETTINGS_BASE_PATH = "Plugins"; static constexpr const char* RESOURCE_OPTION = "path_to_plugin_configuration"; static constexpr const char* DEFAULT_RESOURCE_OPTION = ""; private: String _plugins_base_path; PackedStringArray _dynamic_plugin_list; Array _static_plugins_methods; protected: static void _bind_methods(); }; } VARIANT_ENUM_CAST(godot::FmodStaticPluginMethod::Type); #endif //GODOTFMOD_FMOD_PLUGINS_SETTINGS_H ================================================ FILE: src/resources/fmod_settings.cpp ================================================ #include "fmod_settings.h" #include #include using namespace godot; void FmodGeneralSettings::set_channel_count(const int p_channel_count) { _channel_count = p_channel_count; } int FmodGeneralSettings::get_channel_count() const { return _channel_count; } void FmodGeneralSettings::set_is_live_update_enabled(const bool p_enable_live_update) { _is_live_update_enabled = p_enable_live_update; } bool FmodGeneralSettings::get_is_live_update_enabled() const { return _is_live_update_enabled; } void FmodGeneralSettings::set_is_memory_tracking_enabled(const bool p_enable_memory_tracking) { _is_memory_tracking_enabled = p_enable_memory_tracking; } bool FmodGeneralSettings::get_is_memory_tracking_enabled() const { return _is_memory_tracking_enabled; } void FmodGeneralSettings::set_default_listener_count(int p_listener_count) { _default_listener_count = p_listener_count; } int FmodGeneralSettings::get_default_listener_count() const { return _default_listener_count; } void FmodGeneralSettings::set_banks_path(const String& p_paths) { _banks_path = p_paths; } const String& FmodGeneralSettings::get_banks_path() const { return _banks_path; } void FmodGeneralSettings::set_should_load_by_name(const bool p_should_load_by_name) { _should_load_by_name = p_should_load_by_name; } bool FmodGeneralSettings::get_should_load_by_name() const { return _should_load_by_name; } Ref FmodGeneralSettings::get_from_project_settings() { Ref settings; settings.instantiate(); ProjectSettings* project_settings = ProjectSettings::get_singleton(); String channel_count_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, CHANNEL_COUNT_OPTION); settings->set_channel_count(project_settings->get_setting(channel_count_setting_path, DEFAULT_CHANNEL_COUNT)); String is_liveupdate_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, IS_LIVE_UPDATE_ENABLED_OPTION); settings->set_is_live_update_enabled(project_settings->get_setting(is_liveupdate_setting_path, DEFAULT_IS_LIVEUPDATE)); String is_memory_tracking_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, IS_LIVE_MEMORY_TRACKING_ENABLED_OPTION); settings->set_is_memory_tracking_enabled(project_settings->get_setting(is_memory_tracking_setting_path, DEFAULT_IS_MEMORY_TRACKING)); String default_listener_count_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, DEFAULT_LISTENER_COUNT_OPTION); settings->set_default_listener_count(project_settings->get_setting(default_listener_count_setting_path, DEFAULT_DEFAULT_LISTENER_COUNT)); String banks_paths_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, BANKS_PATH_OPTION); settings->set_banks_path(project_settings->get_setting(banks_paths_setting_path, DEFAULT_BANKS_PATH)); String should_load_by_name_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, INITIALIZE_BASE_PATH, SHOULD_LOAD_BY_NAME); settings->set_should_load_by_name(project_settings->get_setting(should_load_by_name_setting_path, DEFAULT_SHOULD_LOAD_BY_NAME)); return settings; } void FmodGeneralSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_channel_count", "p_channel_count"), &FmodGeneralSettings::set_channel_count); ClassDB::bind_method(D_METHOD("get_channel_count"), &FmodGeneralSettings::get_channel_count); ClassDB::bind_method(D_METHOD("set_is_live_update_enabled", "p_enable_live_update"), &FmodGeneralSettings::set_is_live_update_enabled); ClassDB::bind_method(D_METHOD("get_is_live_update_enabled"), &FmodGeneralSettings::get_is_live_update_enabled); ClassDB::bind_method(D_METHOD("set_is_memory_tracking_enabled", "p_enable_memory_tracking"), &FmodGeneralSettings::set_is_memory_tracking_enabled); ClassDB::bind_method(D_METHOD("get_is_memory_tracking_enabled"), &FmodGeneralSettings::get_is_memory_tracking_enabled); ClassDB::bind_method(D_METHOD("set_default_listener_count", "p_listener_count"), &FmodGeneralSettings::set_default_listener_count); ClassDB::bind_method(D_METHOD("get_default_listener_count"), &FmodGeneralSettings::get_default_listener_count); ClassDB::bind_method(D_METHOD("set_banks_path", "p_paths"), &FmodGeneralSettings::set_banks_path); ClassDB::bind_method(D_METHOD("get_banks_path"), &FmodGeneralSettings::get_banks_path); ClassDB::bind_method(D_METHOD("set_should_load_by_name", "p_should_load_by_name"), &FmodGeneralSettings::set_should_load_by_name); ClassDB::bind_method(D_METHOD("get_should_load_by_name"), &FmodGeneralSettings::get_should_load_by_name); ADD_PROPERTY( PropertyInfo( Variant::INT, "channel_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_channel_count", "get_channel_count" ); ADD_PROPERTY( PropertyInfo( Variant::BOOL, "is_live_update_enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_is_live_update_enabled", "get_is_live_update_enabled" ); ADD_PROPERTY( PropertyInfo( Variant::BOOL, "is_memory_tracking_enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_is_memory_tracking_enabled", "get_is_memory_tracking_enabled" ); ADD_PROPERTY( PropertyInfo( Variant::INT, "default_listener_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_default_listener_count", "get_default_listener_count" ); ADD_PROPERTY( PropertyInfo( Variant::STRING, "banks_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_banks_path", "get_banks_path" ); ADD_PROPERTY( PropertyInfo( Variant::BOOL, "should_load_by_name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_should_load_by_name", "get_should_load_by_name" ); } ================================================ FILE: src/resources/fmod_settings.h ================================================ #ifndef GODOTFMOD_FMOD_SETTINGS_H #define GODOTFMOD_FMOD_SETTINGS_H #include "fmod_studio.h" #include namespace godot { class FmodGeneralSettings : public Resource { GDCLASS(FmodGeneralSettings, Resource) public: void set_channel_count(const int p_channel_count); int get_channel_count() const; void set_is_live_update_enabled(const bool p_enable_live_update); bool get_is_live_update_enabled() const; void set_is_memory_tracking_enabled(const bool p_enable_memory_tracking); bool get_is_memory_tracking_enabled() const; void set_default_listener_count(int p_listener_count); int get_default_listener_count() const; void set_banks_path(const String& p_paths); const String& get_banks_path() const; void set_should_load_by_name(const bool p_should_load_by_name); bool get_should_load_by_name() const; static Ref get_from_project_settings(); static constexpr const char* INITIALIZE_BASE_PATH = "General"; static constexpr const char* CHANNEL_COUNT_OPTION = "channel_count"; static constexpr const char* IS_LIVE_UPDATE_ENABLED_OPTION = "is_live_update_enabled"; static constexpr const char* IS_LIVE_MEMORY_TRACKING_ENABLED_OPTION = "is_memory_tracking_enabled"; static constexpr const char* DEFAULT_LISTENER_COUNT_OPTION = "default_listener_count"; static constexpr const char* BANKS_PATH_OPTION = "banks_path"; static constexpr const char* SHOULD_LOAD_BY_NAME = "should_load_by_name"; static constexpr const int DEFAULT_CHANNEL_COUNT = 1024; static constexpr const bool DEFAULT_IS_LIVEUPDATE = true; static constexpr const bool DEFAULT_IS_MEMORY_TRACKING = false; static constexpr const int DEFAULT_DEFAULT_LISTENER_COUNT = 1; static constexpr const char* DEFAULT_BANKS_PATH = "res://"; static constexpr const bool DEFAULT_SHOULD_LOAD_BY_NAME = false; private: int _channel_count; int _default_listener_count; bool _is_live_update_enabled; bool _is_memory_tracking_enabled; String _banks_path; bool _should_load_by_name; protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_SETTINGS_H ================================================ FILE: src/resources/fmod_software_format_settings.cpp ================================================ #include "fmod_software_format_settings.h" #include #include using namespace godot; void FmodSoftwareFormatSettings::set_sample_rate(const int p_sample_rate) { _sample_rate = p_sample_rate; } int FmodSoftwareFormatSettings::get_sample_rate() const { return _sample_rate; } void FmodSoftwareFormatSettings::set_speaker_mode(const int p_speaker_mode) { _speaker_mode = p_speaker_mode; } int FmodSoftwareFormatSettings::get_speaker_mode() const { return _speaker_mode; } void FmodSoftwareFormatSettings::set_raw_speakers_count(const int p_raw_speakers_count) { _raw_speakers_count = p_raw_speakers_count; } int FmodSoftwareFormatSettings::get_raw_speakers_count() const { return _raw_speakers_count; } Ref FmodSoftwareFormatSettings::get_from_project_settings() { Ref settings; settings.instantiate(); ProjectSettings* project_settings = ProjectSettings::get_singleton(); String sample_rate_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, SOFTWARE_FORMAT_SETTINGS_BASE_PATH, SAMPLE_RATE_OPTION); settings->set_sample_rate(project_settings->get_setting(sample_rate_setting_path, DEFAULT_SAMPLE_RATE)); String speaker_mode_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, SOFTWARE_FORMAT_SETTINGS_BASE_PATH, SPEAKER_MODE_OPTION); settings->set_speaker_mode(project_settings->get_setting(speaker_mode_setting_path, DEFAULT_SPEAKER_MODE)); String raw_speaker_count_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, SOFTWARE_FORMAT_SETTINGS_BASE_PATH, RAW_SPEAKER_COUNT_OPTION); settings->set_raw_speakers_count(project_settings->get_setting(raw_speaker_count_setting_path, DEFAULT_RAW_SPEAKER_COUNT)); return settings; } void FmodSoftwareFormatSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_sample_rate", "p_sample_rate"), &FmodSoftwareFormatSettings::set_sample_rate); ClassDB::bind_method(D_METHOD("get_sample_rate"), &FmodSoftwareFormatSettings::get_sample_rate); ClassDB::bind_method(D_METHOD("set_speaker_mode", "p_speaker_mode"), &FmodSoftwareFormatSettings::set_speaker_mode); ClassDB::bind_method(D_METHOD("get_speaker_mode"), &FmodSoftwareFormatSettings::get_speaker_mode); ClassDB::bind_method(D_METHOD("set_raw_speakers_count", "p_raw_speakers_count"), &FmodSoftwareFormatSettings::set_raw_speakers_count); ClassDB::bind_method(D_METHOD("get_raw_speakers_count"), &FmodSoftwareFormatSettings::get_raw_speakers_count); ADD_PROPERTY( PropertyInfo( Variant::INT, "sample_rate", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_sample_rate", "get_sample_rate" ); ADD_PROPERTY( PropertyInfo( Variant::INT, "speaker_mode", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT ), "set_speaker_mode", "get_speaker_mode" ); ADD_PROPERTY( PropertyInfo( Variant::INT, "raw_speakers_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_raw_speakers_count", "get_raw_speakers_count" ); } ================================================ FILE: src/resources/fmod_software_format_settings.h ================================================ #ifndef GODOTFMOD_FMOD_SOFTWARE_FORMAT_SETTINGS_H #define GODOTFMOD_FMOD_SOFTWARE_FORMAT_SETTINGS_H #include "fmod_common.h" #include namespace godot { class FmodSoftwareFormatSettings : public Resource { GDCLASS(FmodSoftwareFormatSettings, Resource) public: void set_sample_rate(const int p_sample_rate); int get_sample_rate() const; void set_speaker_mode(const int p_speaker_mode); int get_speaker_mode() const; void set_raw_speakers_count(const int p_raw_speakers_count); int get_raw_speakers_count() const; static Ref get_from_project_settings(); FmodSoftwareFormatSettings() = default; ~FmodSoftwareFormatSettings() = default; static constexpr const char* SOFTWARE_FORMAT_SETTINGS_BASE_PATH = "Software Format"; static constexpr const char* SAMPLE_RATE_OPTION = "sample_rate"; static constexpr const char* SPEAKER_MODE_OPTION = "speaker_mode"; static constexpr const char* RAW_SPEAKER_COUNT_OPTION = "raw_speaker_count"; static constexpr const int DEFAULT_SAMPLE_RATE = 48000; static constexpr const int DEFAULT_SPEAKER_MODE = FMOD_SPEAKERMODE::FMOD_SPEAKERMODE_STEREO; static constexpr const int DEFAULT_RAW_SPEAKER_COUNT = 0; private: int _sample_rate; int _speaker_mode; int _raw_speakers_count; protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_SOFTWARE_FORMAT_SETTINGS_H ================================================ FILE: src/resources/fmod_sound_3d_settings.cpp ================================================ #include "fmod_sound_3d_settings.h" #include #include using namespace godot; void FmodSound3DSettings::set_doppler_scale(const float p_doppler_scale) { _doppler_scale = p_doppler_scale; } float FmodSound3DSettings::get_doppler_scale() const { return _doppler_scale; } void FmodSound3DSettings::set_distance_factor(const float p_distance_factor) { _distance_factor = p_distance_factor; } float FmodSound3DSettings::get_distance_factor() const { return _distance_factor; } void FmodSound3DSettings::set_rolloff_scale(const float p_rolloff_scale) { _rolloff_scale = p_rolloff_scale; } float FmodSound3DSettings::get_rolloff_scale() const { return _rolloff_scale; } Ref FmodSound3DSettings::get_from_project_settings() { Ref settings; settings.instantiate(); ProjectSettings* project_settings = ProjectSettings::get_singleton(); String doppler_scale_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, THREE_D_SETTINGS_BASE_PATH, DOPPLER_SCALE_OPTION); settings->set_doppler_scale(project_settings->get_setting(doppler_scale_setting_path, DEFAULT_DOPPLER_SCALE)); String distance_factor_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, THREE_D_SETTINGS_BASE_PATH, DISTANCE_FACTOR_OPTION); settings->set_distance_factor(project_settings->get_setting(distance_factor_setting_path, DEFAULT_DISTANCE_FACTOR)); String rolloff_scale_setting_path = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, THREE_D_SETTINGS_BASE_PATH, ROLLOFF_SCALE_OPTION); settings->set_rolloff_scale(project_settings->get_setting(rolloff_scale_setting_path, DEFAULT_ROLLOFF_SCALE)); return settings; } void FmodSound3DSettings::_bind_methods() { ClassDB::bind_method(D_METHOD("set_doppler_scale", "p_doppler_scale"), &FmodSound3DSettings::set_doppler_scale); ClassDB::bind_method(D_METHOD("get_doppler_scale"), &FmodSound3DSettings::get_doppler_scale); ClassDB::bind_method(D_METHOD("set_distance_factor", "p_distance_factor"), &FmodSound3DSettings::set_distance_factor); ClassDB::bind_method(D_METHOD("get_distance_factor"), &FmodSound3DSettings::get_distance_factor); ClassDB::bind_method(D_METHOD("set_rolloff_scale", "p_rolloff_scale"), &FmodSound3DSettings::set_rolloff_scale); ClassDB::bind_method(D_METHOD("get_rolloff_scale"), &FmodSound3DSettings::get_rolloff_scale); ADD_PROPERTY( PropertyInfo( Variant::FLOAT, "doppler_scale", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_doppler_scale", "get_doppler_scale" ); ADD_PROPERTY( PropertyInfo( Variant::FLOAT, "distance_factor", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_distance_factor", "get_distance_factor" ); ADD_PROPERTY( PropertyInfo( Variant::FLOAT, "rolloff_scale", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT ), "set_rolloff_scale", "get_rolloff_scale" ); } ================================================ FILE: src/resources/fmod_sound_3d_settings.h ================================================ #ifndef GODOTFMOD_FMOD_SOUND_3D_SETTINGS_H #define GODOTFMOD_FMOD_SOUND_3D_SETTINGS_H #include namespace godot { class FmodSound3DSettings : public Resource { GDCLASS(FmodSound3DSettings, Resource) public: void set_doppler_scale(const float p_doppler_scale); float get_doppler_scale() const; void set_distance_factor(const float p_distance_factor); float get_distance_factor() const; void set_rolloff_scale(const float p_rolloff_scale); float get_rolloff_scale() const; static Ref get_from_project_settings(); FmodSound3DSettings() = default; ~FmodSound3DSettings() = default; static constexpr const char* THREE_D_SETTINGS_BASE_PATH = "3D Settings"; static constexpr const char* DOPPLER_SCALE_OPTION = "doppler_scale"; static constexpr const char* DISTANCE_FACTOR_OPTION = "distance_factor"; static constexpr const char* ROLLOFF_SCALE_OPTION = "rolloff_scale"; static constexpr const float DEFAULT_DOPPLER_SCALE = 1; static constexpr const float DEFAULT_DISTANCE_FACTOR = 1; static constexpr const float DEFAULT_ROLLOFF_SCALE = 1; private: float _doppler_scale; float _distance_factor; float _rolloff_scale; protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_SOUND_3D_SETTINGS_H ================================================ FILE: src/studio/fmod_bank.cpp ================================================ #include "fmod_bank.h" #include "helpers/common.h" #include "fmod_server.h" using namespace godot; void FmodBank::_bind_methods() { ClassDB::bind_method(D_METHOD("get_loading_state"), &FmodBank::get_loading_state); ClassDB::bind_method(D_METHOD("get_bus_count"), &FmodBank::get_bus_count); ClassDB::bind_method(D_METHOD("get_event_description_count"), &FmodBank::get_event_description_count); ClassDB::bind_method(D_METHOD("get_string_count"), &FmodBank::get_string_count); ClassDB::bind_method(D_METHOD("get_VCA_count"), &FmodBank::get_vca_count); ClassDB::bind_method(D_METHOD("get_description_list"), &FmodBank::get_description_list); ClassDB::bind_method(D_METHOD("get_bus_list"), &FmodBank::get_bus_list); ClassDB::bind_method(D_METHOD("get_vca_list"), &FmodBank::get_vca_list); ClassDB::bind_method(D_METHOD("is_valid"), &FmodBank::is_valid); ClassDB::bind_method(D_METHOD("get_godot_res_path"), &FmodBank::get_godot_res_path); ClassDB::bind_method(D_METHOD("get_path"), &FmodBank::get_path); ClassDB::bind_method(D_METHOD("get_guid"), &FmodBank::get_guid_as_string); } int FmodBank::get_loading_state() { FMOD_STUDIO_LOADING_STATE state; ERROR_CHECK_WITH_REASON(_wrapped->getLoadingState(&state), vformat("Cannot get loading state for bank %s", get_path())); return state; } int64_t FmodBank::get_bus_count() { return _buses.size(); } int64_t FmodBank::get_event_description_count() { return _event_descriptions.size(); } int64_t FmodBank::get_vca_count() const { return _vcas.size(); } int FmodBank::get_string_count() const { int count = -1; ERROR_CHECK_WITH_REASON(_wrapped->getStringCount(&count), vformat("Cannot get string count for bank %s", get_path())); return count; } Array FmodBank::get_description_list() const { Array array; for (const Ref& ref : _event_descriptions) { array.append(ref); } return array; } Array FmodBank::get_bus_list() const { Array array; for (const Ref& ref : _buses) { array.append(ref); } return array; } Array FmodBank::get_vca_list() const { Array array; for (const Ref& ref : _vcas) { array.append(ref); } return array; } void FmodBank::update_bank_data() { load_all_buses(); load_all_vca(); load_all_event_descriptions(); } void FmodBank::load_all_vca() { int size = 0; if (ERROR_CHECK_WITH_REASON(_wrapped->getVCACount(&size), vformat("Cannot get VCA count for bank %s", get_path()))) { if (size == 0) { return; } Vector raw_vcas; raw_vcas.resize(size); if (ERROR_CHECK_WITH_REASON(_wrapped->getVCAList(raw_vcas.ptrw(), size, &size), vformat("Cannot get VCA list for bank %s", get_path()))) { _vcas.clear(); for (int i = 0; i < size; ++i) { Ref ref = FmodVCA::create_ref(raw_vcas[i]); _vcas.push_back(ref); } } } } void FmodBank::load_all_buses() { int size = 0; if (ERROR_CHECK_WITH_REASON(_wrapped->getBusCount(&size), vformat("Cannot get bus count for bank %s", get_path()))) { if (size == 0) { return; } Vector raw_buses; raw_buses.resize(size); if (ERROR_CHECK_WITH_REASON(_wrapped->getBusList(raw_buses.ptrw(), size, &size), vformat("Cannot get bus list for bank %s", get_path()))) { _buses.clear(); for (int i = 0; i < size; ++i) { Ref ref = FmodBus::create_ref(raw_buses[i]); _buses.push_back(ref); } } } } void FmodBank::load_all_event_descriptions() { int size = 0; if (ERROR_CHECK_WITH_REASON(_wrapped->getEventCount(&size), vformat("Cannot get event count for bank %s", get_path()))) { if (size == 0) { return; } Vector raw_events; raw_events.resize(size); if (ERROR_CHECK_WITH_REASON(_wrapped->getEventList(raw_events.ptrw(), size, &size), vformat("Cannot get event list for bank %s", get_path()))) { _event_descriptions.clear(); for (int i = 0; i < size; ++i) { Ref ref = FmodEventDescription::create_ref(raw_events[i]); _event_descriptions.push_back(ref); } } } } const List>& FmodBank::get_event_descriptions() const { return _event_descriptions; } const List>& FmodBank::get_buses() const { return _buses; } const List>& FmodBank::get_vcas() const { return _vcas; } const String& FmodBank::get_godot_res_path() const { return _godot_res_path; } FmodBank::~FmodBank() { FmodServer::get_singleton()->unload_bank(_godot_res_path); } ================================================ FILE: src/studio/fmod_bank.h ================================================ #ifndef GODOTFMOD_FMOD_BANK_H #define GODOTFMOD_FMOD_BANK_H #include "classes/ref_counted.hpp" #include "fmod_bus.h" #include "fmod_event_description.h" #include "fmod_studio.hpp" #include "fmod_vca.h" #include "helpers/common.h" namespace godot { class FmodBank : public RefCounted { FMODCLASSWITHPATH(FmodBank, RefCounted, FMOD::Studio::Bank); List> _event_descriptions; List> _buses; List> _vcas; String _godot_res_path; void load_all_vca(); void load_all_buses(); void load_all_event_descriptions(); public: FmodBank() = default; ~FmodBank() override; int get_loading_state(); int64_t get_event_description_count(); int64_t get_bus_count(); int64_t get_vca_count() const; int get_string_count() const; Array get_description_list() const; Array get_bus_list() const; Array get_vca_list() const; void update_bank_data(); const List>& get_event_descriptions() const; const List>& get_buses() const; const List>& get_vcas() const; const String& get_godot_res_path() const; inline static Ref create_ref(FMOD::Studio::Bank* wrapped, const String& p_godot_res_path) { Ref ref { create_ref(wrapped) }; ref->_godot_res_path = p_godot_res_path; return ref; } protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_BANK_H ================================================ FILE: src/studio/fmod_bus.cpp ================================================ #include "fmod_bus.h" #include "helpers/common.h" using namespace godot; void FmodBus::_bind_methods() { ClassDB::bind_method(D_METHOD("get_mute"), &FmodBus::get_mute); ClassDB::bind_method(D_METHOD("get_paused"), &FmodBus::get_paused); ClassDB::bind_method(D_METHOD("get_volume"), &FmodBus::get_volume); ClassDB::bind_method(D_METHOD("set_mute", "mute"), &FmodBus::set_mute); ClassDB::bind_method(D_METHOD("set_paused", "paused"), &FmodBus::set_paused); ClassDB::bind_method(D_METHOD("set_volume", "volume"), &FmodBus::set_volume); ClassDB::bind_method(D_METHOD("stop_all_events", "stopMode"), &FmodBus::stop_all_events); ClassDB::bind_method(D_METHOD("is_valid"), &FmodBus::is_valid); ClassDB::bind_method(D_METHOD("get_path"), &FmodBus::get_path); ClassDB::bind_method(D_METHOD("get_guid"), &FmodBus::get_guid_as_string); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "mute",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_mute", "get_mute"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "paused",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_paused", "get_paused"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_volume", "get_volume"); } bool FmodBus::get_mute() const { bool mute = false; ERROR_CHECK_WITH_REASON(_wrapped->getMute(&mute), vformat("Cannot check mute for bus %s", get_path())); return mute; } bool FmodBus::get_paused() const { bool paused = false; ERROR_CHECK_WITH_REASON(_wrapped->getPaused(&paused), vformat("Cannot check paused for bus %s", get_path())); return paused; } float FmodBus::get_volume() const { float volume = 0.0f; ERROR_CHECK_WITH_REASON(_wrapped->getVolume(&volume), vformat("Cannot get volume for bus %s", get_path())); return volume; } void FmodBus::set_mute(bool mute) const { ERROR_CHECK_WITH_REASON(_wrapped->setMute(mute), vformat("Cannot mute bus %s", get_path())); } void FmodBus::set_paused(bool paused) const { ERROR_CHECK_WITH_REASON(_wrapped->setPaused(paused), vformat("Cannot pause bus %s", get_path())); } void FmodBus::set_volume(float volume) const { ERROR_CHECK_WITH_REASON(_wrapped->setVolume(volume), vformat("Cannot set bus %s volume to %f", get_path(), volume)); } void FmodBus::stop_all_events(int stopMode) { ERROR_CHECK_WITH_REASON(_wrapped->stopAllEvents(static_cast(stopMode)), vformat("Cannot stop all bus %s events", get_path())); } ================================================ FILE: src/studio/fmod_bus.h ================================================ #ifndef GODOTFMOD_FMOD_BUS_H #define GODOTFMOD_FMOD_BUS_H #include "fmod_studio.hpp" #include "helpers/common.h" namespace godot { class FmodBus : public RefCounted { FMODCLASSWITHPATH(FmodBus, RefCounted, FMOD::Studio::Bus); public: FmodBus() = default; ~FmodBus() override = default; bool get_mute() const; bool get_paused() const; float get_volume() const; void set_mute(bool mute) const; void set_paused(bool paused) const; void set_volume(float volume) const; void stop_all_events(int stopMode); protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_BUS_H ================================================ FILE: src/studio/fmod_event.cpp ================================================ #include "fmod.h" #include "fmod_server.h" #include "helpers/common.h" #include "helpers/maths.h" #include "fmod_event.h" using namespace godot; void FmodEvent::_bind_methods() { ClassDB::bind_method(D_METHOD("get_parameter_by_name", "parameter_name"), &FmodEvent::get_parameter_by_name); ClassDB::bind_method(D_METHOD("set_parameter_by_name", "parameter_name", "value"), &FmodEvent::set_parameter_by_name); ClassDB::bind_method(D_METHOD("set_parameter_by_name_with_label", "parameter_name", "label", "ignoreseekspeed"), &FmodEvent::set_parameter_by_name_with_label); ClassDB::bind_method(D_METHOD("get_parameter_by_id", "parameter_id"), &FmodEvent::get_parameter_by_id); ClassDB::bind_method(D_METHOD("set_parameter_by_id", "parameter_id", "value"), &FmodEvent::set_parameter_by_id); ClassDB::bind_method(D_METHOD("set_parameter_by_id_with_label", "parameter_id", "label", "ignoreseekspeed"), &FmodEvent::set_parameter_by_id_with_label); ClassDB::bind_method(D_METHOD("start"), &FmodEvent::start); ClassDB::bind_method(D_METHOD("stop", "stopMode"), &FmodEvent::stop); ClassDB::bind_method(D_METHOD("event_key_off"), &FmodEvent::event_key_off); ClassDB::bind_method(D_METHOD("get_playback_state"), &FmodEvent::get_playback_state); ClassDB::bind_method(D_METHOD("get_paused"), &FmodEvent::get_paused); ClassDB::bind_method(D_METHOD("set_paused", "paused"), &FmodEvent::set_paused); ClassDB::bind_method(D_METHOD("get_pitch"), &FmodEvent::get_pitch); ClassDB::bind_method(D_METHOD("set_pitch", "pitch"), &FmodEvent::set_pitch); ClassDB::bind_method(D_METHOD("get_volume"), &FmodEvent::get_volume); ClassDB::bind_method(D_METHOD("set_volume", "volume"), &FmodEvent::set_volume); ClassDB::bind_method(D_METHOD("get_timeline_position"), &FmodEvent::get_timeline_position); ClassDB::bind_method(D_METHOD("set_timeline_position", "position"), &FmodEvent::set_timeline_position); ClassDB::bind_method(D_METHOD("get_reverb_level", "index"), &FmodEvent::get_reverb_level); ClassDB::bind_method(D_METHOD("set_reverb_level", "index", "level"), &FmodEvent::set_reverb_level); ClassDB::bind_method(D_METHOD("is_virtual"), &FmodEvent::is_virtual); ClassDB::bind_method(D_METHOD("set_listener_mask", "mask"), &FmodEvent::set_listener_mask); ClassDB::bind_method(D_METHOD("get_listener_mask"), &FmodEvent::get_listener_mask); ClassDB::bind_method(D_METHOD("set_2d_attributes", "position"), &FmodEvent::set_2d_attributes); ClassDB::bind_method(D_METHOD("get_2d_attributes"), &FmodEvent::get_2d_attributes); ClassDB::bind_method(D_METHOD("set_3d_attributes", "transform"), &FmodEvent::set_3d_attributes); ClassDB::bind_method(D_METHOD("get_3d_attributes"), &FmodEvent::get_3d_attributes); ClassDB::bind_method(D_METHOD("set_node_attributes", "transform"), &FmodEvent::set_node_attributes); ClassDB::bind_method(D_METHOD("set_callback", "callback", "callbackMask"), &FmodEvent::set_callback); ClassDB::bind_method(D_METHOD("set_programmer_callback", "p_programmers_callback_sound_key"), &FmodEvent::set_programmer_callback); ClassDB::bind_method(D_METHOD("get_programmer_callback_sound_key"), &FmodEvent::get_programmers_callback_sound_key); ClassDB::bind_method(D_METHOD("is_valid"), &FmodEvent::is_valid); ClassDB::bind_method(D_METHOD("release"), &FmodEvent::release); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "paused",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_paused", "get_paused"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_pitch", "get_pitch"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_volume", "get_volume"); ADD_PROPERTY(PropertyInfo(Variant::INT, "position",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_timeline_position", "get_timeline_position"); ADD_PROPERTY(PropertyInfo(Variant::INT, "listener_mask",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_listener_mask", "get_listener_mask"); ADD_PROPERTY(PropertyInfo(Variant::TRANSFORM2D, "transform_2d",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_2d_attributes", "get_2d_attributes"); ADD_PROPERTY(PropertyInfo(Variant::TRANSFORM3D, "transform_3d",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_node_attributes", "get_3d_attributes"); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_PLAYING); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_SUSTAINING); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_STOPPED); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_STARTING); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_STOPPING); BIND_ENUM_CONSTANT(FMOD_STUDIO_PLAYBACK_FORCEINT); } float FmodEvent::get_parameter_by_name(const String& parameter_name) const { float p = -1; ERROR_CHECK_WITH_REASON(_wrapped->getParameterByName(parameter_name.utf8().get_data(), &p), vformat("Cannot get parameter %s", parameter_name)); return p; } void FmodEvent::set_parameter_by_name(const String& parameter_name, float value) const { ERROR_CHECK_WITH_REASON(_wrapped->setParameterByName(parameter_name.utf8().get_data(), value), vformat("Cannot set parameter %s to value %f", parameter_name, value)); } void FmodEvent::set_parameter_by_name_with_label(const String& parameter_name, const String& label, bool ignoreseekspeed) const { ERROR_CHECK_WITH_REASON(_wrapped->setParameterByNameWithLabel(parameter_name.utf8().get_data(), label.utf8().get_data(), ignoreseekspeed), vformat("Cannot set parameter %s value to %s", parameter_name, label)); } float FmodEvent::get_parameter_by_id(uint64_t long_id) const { return get_parameter_by_fmod_id(ulong_to_fmod_parameter_id(long_id)); } float FmodEvent::get_parameter_by_fmod_id(const FMOD_STUDIO_PARAMETER_ID& parameter_id) const { float value = -1.0f; ERROR_CHECK_WITH_REASON(_wrapped->getParameterByID(parameter_id, &value), vformat("Cannot get parameter with id: %d", fmod_parameter_id_to_ulong(parameter_id))); return value; } void FmodEvent::set_parameter_by_id(uint64_t long_id, float value) const { set_parameter_by_fmod_id(ulong_to_fmod_parameter_id(long_id), value); } void FmodEvent::set_parameter_by_fmod_id(const FMOD_STUDIO_PARAMETER_ID& parameter_id, float value) const { ERROR_CHECK_WITH_REASON(_wrapped->setParameterByID(parameter_id, value), vformat("Cannot set parameter with id %d to value %f", fmod_parameter_id_to_ulong(parameter_id), value)); } void FmodEvent::set_parameter_by_fmod_id_with_label(const FMOD_STUDIO_PARAMETER_ID& parameter_id, const String& label, bool ignoreseekspeed) const { ERROR_CHECK_WITH_REASON(_wrapped->setParameterByIDWithLabel(parameter_id, label.utf8().get_data(), ignoreseekspeed), vformat("Cannot set parameter to id %d to value %s", fmod_parameter_id_to_ulong(parameter_id), label)); } void FmodEvent::set_parameter_by_id_with_label(uint64_t parameter_id, const String& label, bool ignoreseekspeed) const { set_parameter_by_fmod_id_with_label(ulong_to_fmod_parameter_id(parameter_id), label, ignoreseekspeed); } void FmodEvent::release() const { ERROR_CHECK(_wrapped->release()); } void FmodEvent::start() const { ERROR_CHECK(_wrapped->start()); } void FmodEvent::stop(int stopMode) const { ERROR_CHECK(_wrapped->stop(static_cast(stopMode))); } void FmodEvent::event_key_off() const { ERROR_CHECK(_wrapped->keyOff()); } FMOD_STUDIO_PLAYBACK_STATE FmodEvent::get_playback_state() const { FMOD_STUDIO_PLAYBACK_STATE playback_state; ERROR_CHECK(_wrapped->getPlaybackState(&playback_state)); return playback_state; } bool FmodEvent::get_paused() const { bool paused = false; ERROR_CHECK(_wrapped->getPaused(&paused)); return paused; } void FmodEvent::set_paused(bool paused) const { ERROR_CHECK(_wrapped->setPaused(paused)); } float FmodEvent::get_pitch() const { float pitch = 0.0f; ERROR_CHECK(_wrapped->getPitch(&pitch)); return pitch; } void FmodEvent::set_pitch(float pitch) const { ERROR_CHECK(_wrapped->setPitch(pitch)); } float FmodEvent::get_volume() { float volume = 0.0f; ERROR_CHECK(_wrapped->getVolume(&volume)); return volume; } void FmodEvent::set_volume(float volume) const { ERROR_CHECK(_wrapped->setVolume(volume)); } int FmodEvent::get_timeline_position() const { int tp = 0; ERROR_CHECK(_wrapped->getTimelinePosition(&tp)); return tp; } void FmodEvent::set_timeline_position(int position) const { ERROR_CHECK(_wrapped->setTimelinePosition(position)); } float FmodEvent::get_reverb_level(int index) const { float rvl = 0.0f; ERROR_CHECK(_wrapped->getReverbLevel(index, &rvl)); return rvl; } void FmodEvent::set_reverb_level(int index, float level) const { ERROR_CHECK(_wrapped->setReverbLevel(index, level)); } bool FmodEvent::is_virtual() const { bool v = false; ERROR_CHECK(_wrapped->isVirtual(&v)); return v; } void FmodEvent::set_listener_mask(unsigned int mask) const { ERROR_CHECK(_wrapped->setListenerMask(mask)); } uint32_t FmodEvent::get_listener_mask() const { uint32_t mask = 0; ERROR_CHECK(_wrapped->getListenerMask(&mask)); return mask; } void FmodEvent::set_2d_attributes(const Transform2D& position) const { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform2d(position, distanceScale); ERROR_CHECK(_wrapped->set3DAttributes(&attr)); } Transform2D FmodEvent::get_2d_attributes() const { Transform2D _2Dattr; FMOD_3D_ATTRIBUTES attr; ERROR_CHECK(_wrapped->get3DAttributes(&attr)); _2Dattr = get_transform2d_from_3d_attributes(attr, distanceScale); return _2Dattr; } void FmodEvent::set_3d_attributes(const Transform3D& transform) const { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform3d(transform, distanceScale); ERROR_CHECK(_wrapped->set3DAttributes(&attr)); } Transform3D FmodEvent::get_3d_attributes() const { Transform3D _3Dattr; FMOD_3D_ATTRIBUTES attr; ERROR_CHECK(_wrapped->get3DAttributes(&attr)); _3Dattr = get_transform3d_from_3d_attributes(attr, distanceScale); return _3Dattr; } void FmodEvent::set_node_attributes(Node* node) const { if (node->is_inside_tree()) { if (auto* ci {Node::cast_to(node)}) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform2d(ci->get_global_transform(), distanceScale); ERROR_CHECK(_wrapped->set3DAttributes(&attr)); return; } if (auto* s {Node::cast_to(node)}) { FMOD_3D_ATTRIBUTES attr = get_3d_attributes_from_transform3d(s->get_global_transform(), distanceScale); ERROR_CHECK(_wrapped->set3DAttributes(&attr)); return; } } GODOT_LOG_ERROR("Invalid Object. A Godot object bound to FMOD has to be either a Node3D or CanvasItem.") } void FmodEvent::set_callback(const Callable& callback, uint32_t p_callback_mask) { eventCallback = callback; callback_mask = p_callback_mask; ERROR_CHECK(_wrapped->setCallback(Callbacks::event_callback, p_callback_mask)); } void FmodEvent::set_programmer_callback(const String& p_programmers_callback_sound_key) { programmers_callback_sound_key = p_programmers_callback_sound_key; ERROR_CHECK(_wrapped->setCallback(Callbacks::event_callback, callback_mask | FMOD_STUDIO_EVENT_CALLBACK_CREATE_PROGRAMMER_SOUND | FMOD_STUDIO_EVENT_CALLBACK_DESTROY_PROGRAMMER_SOUND)); } const Callable& FmodEvent::get_callback() const { return eventCallback; } const String& FmodEvent::get_programmers_callback_sound_key() const { return programmers_callback_sound_key; } void FmodEvent::set_distance_scale(float scale){ distanceScale = scale; } FmodEvent::~FmodEvent() { if (is_valid()) { _wrapped->setUserData(nullptr); } } ================================================ FILE: src/studio/fmod_event.h ================================================ #ifndef GODOTFMOD_FMOD_EVENT_H #define GODOTFMOD_FMOD_EVENT_H #include "classes/ref_counted.hpp" #include "fmod_studio.hpp" #include "helpers/common.h" namespace godot { class FmodEvent : public RefCounted { FMODCLASS(FmodEvent, RefCounted, FMOD::Studio::EventInstance); Callable eventCallback; String programmers_callback_sound_key; float distanceScale = 1.0f; uint32_t callback_mask; public: FmodEvent() = default; ~FmodEvent() override; float get_parameter_by_name(const String& parameter_name) const; void set_parameter_by_name(const String& parameter_name, float value) const; void set_parameter_by_name_with_label(const String& parameter_name, const String& label, bool ignoreseekspeed = false) const; float get_parameter_by_id(uint64_t long_id) const; float get_parameter_by_fmod_id(const FMOD_STUDIO_PARAMETER_ID& parameter_id) const; void set_parameter_by_id(uint64_t long_id, float value) const; void set_parameter_by_fmod_id(const FMOD_STUDIO_PARAMETER_ID& parameter_id, float value) const; void set_parameter_by_fmod_id_with_label(const FMOD_STUDIO_PARAMETER_ID& parameter_id, const String& label, bool ignoreseekspeed = false) const; void set_parameter_by_id_with_label(uint64_t parameter_id, const String& label, bool ignoreseekspeed = false) const; void release() const; void start() const; void stop(int stopMode) const; void event_key_off() const; FMOD_STUDIO_PLAYBACK_STATE get_playback_state() const; bool get_paused() const; void set_paused(bool paused) const; float get_pitch() const; void set_pitch(float pitch) const; float get_volume(); void set_volume(float volume) const; int get_timeline_position() const; void set_timeline_position(int position) const; float get_reverb_level(int index) const; void set_reverb_level(int index, float level) const; bool is_virtual() const; void set_listener_mask(unsigned int mask) const; uint32_t get_listener_mask() const; Transform3D get_3d_attributes() const; Transform2D get_2d_attributes() const; void set_2d_attributes(const Transform2D& position) const; void set_3d_attributes(const Transform3D& transform) const; void set_node_attributes(Node* node) const; void set_callback(const Callable& callback, uint32_t p_callback_mask); const Callable& get_callback() const; void set_programmer_callback(const String& p_programmers_callback_sound_key); const String& get_programmers_callback_sound_key() const; void set_distance_scale(float scale); protected: static void _bind_methods(); }; }// namespace godot VARIANT_ENUM_CAST(FMOD_STUDIO_PLAYBACK_STATE) #endif// GODOTFMOD_FMOD_EVENT_H ================================================ FILE: src/studio/fmod_event_description.cpp ================================================ #include "fmod_event_description.h" #include "fmod_event.h" #include "fmod_parameter_description.h" #include "helpers/common.h" using namespace godot; constexpr const uint32_t PARAMETER_LABEL_BUFFER_SIZE {256}; void FmodEventDescription::_bind_methods() { ClassDB::bind_method(D_METHOD("get_length"), &FmodEventDescription::get_length); ClassDB::bind_method(D_METHOD("get_instance_list"), &FmodEventDescription::get_instance_list); ClassDB::bind_method(D_METHOD("get_instance_count"), &FmodEventDescription::get_instance_count); ClassDB::bind_method(D_METHOD("release_all_instances"), &FmodEventDescription::release_all_instances); ClassDB::bind_method(D_METHOD("load_sample_data"), &FmodEventDescription::load_sample_data); ClassDB::bind_method(D_METHOD("unload_sample_data"), &FmodEventDescription::unload_sample_data); ClassDB::bind_method(D_METHOD("get_sample_loading_state"), &FmodEventDescription::get_sample_loading_state); ClassDB::bind_method(D_METHOD("is_3d"), &FmodEventDescription::is_3d); ClassDB::bind_method(D_METHOD("is_one_shot"), &FmodEventDescription::is_one_shot); ClassDB::bind_method(D_METHOD("is_snapshot"), &FmodEventDescription::is_snapshot); ClassDB::bind_method(D_METHOD("is_stream"), &FmodEventDescription::is_stream); ClassDB::bind_method(D_METHOD("has_sustain_point"), &FmodEventDescription::has_sustain_point); ClassDB::bind_method(D_METHOD("get_min_max_distance"), &FmodEventDescription::get_min_max_distance); ClassDB::bind_method(D_METHOD("get_sound_size"), &FmodEventDescription::get_sound_size); ClassDB::bind_method(D_METHOD("get_parameter_by_name", "name"), &FmodEventDescription::get_parameter_by_name); ClassDB::bind_method( D_METHOD( "get_parameter_by_id", "eventPath" "idPair" ), &FmodEventDescription::get_parameter_by_id ); ClassDB::bind_method(D_METHOD("get_parameter_count"), &FmodEventDescription::get_parameter_count); ClassDB::bind_method(D_METHOD("get_parameter_by_index", "index"), &FmodEventDescription::get_parameter_by_index); ClassDB::bind_method(D_METHOD("get_parameters"), &FmodEventDescription::get_parameters); ClassDB::bind_method(D_METHOD("get_parameter_label_by_id"), &FmodEventDescription::get_parameter_label_by_id); ClassDB::bind_method(D_METHOD("get_parameter_label_by_name"), &FmodEventDescription::get_parameter_label_by_name); ClassDB::bind_method(D_METHOD("get_parameter_label_by_index"), &FmodEventDescription::get_parameter_label_by_index); ClassDB::bind_method(D_METHOD("get_parameter_labels_by_id"), &FmodEventDescription::get_parameter_labels_by_id); ClassDB::bind_method(D_METHOD("get_parameter_labels_by_name"), &FmodEventDescription::get_parameter_labels_by_name); ClassDB::bind_method(D_METHOD("get_parameter_labels_by_index"), &FmodEventDescription::get_parameter_labels_by_index); ClassDB::bind_method(D_METHOD("get_user_property", "name"), &FmodEventDescription::get_user_property); ClassDB::bind_method(D_METHOD("get_user_property_count"), &FmodEventDescription::get_user_property_count); ClassDB::bind_method(D_METHOD("user_property_by_index", "index"), &FmodEventDescription::user_property_by_index); ClassDB::bind_method(D_METHOD("is_valid"), &FmodEventDescription::is_valid); ClassDB::bind_method(D_METHOD("get_path"), &FmodEventDescription::get_path); ClassDB::bind_method(D_METHOD("get_guid"), &FmodEventDescription::get_guid_as_string); } int FmodEventDescription::get_length() { int length = -1; ERROR_CHECK_WITH_REASON(_wrapped->getLength(&length), vformat("Cannot get event %s with guid %s length.", get_path(), get_guid_as_string())); return length; } Array FmodEventDescription::get_instance_list() { Array array; int size = 0; if (ERROR_CHECK_WITH_REASON(_wrapped->getInstanceCount(&size), vformat("Cannot get instances count for event %s with guid %s", get_path(), get_guid_as_string()))) { Vector instances; instances.resize(size); if (ERROR_CHECK_WITH_REASON(_wrapped->getInstanceList(instances.ptrw(), size, &size), vformat("Cannot get instances list for event %s with guid %s", get_path(), get_guid_as_string()))) { for (int i = 0; i < size; ++i) { godot::FmodEvent* event_instance; instances[i]->getUserData((void**) &event_instance); array.append(Ref(event_instance)); } } } return array; } int FmodEventDescription::get_instance_count() { int count = -1; ERROR_CHECK_WITH_REASON(_wrapped->getInstanceCount(&count), vformat("Cannot get instance count for event %s with guid %s", get_path(), get_guid_as_string())); return count; } void FmodEventDescription::release_all_instances() { ERROR_CHECK_WITH_REASON(_wrapped->releaseAllInstances(), vformat("Cannot get release all instances for event %s with guid %s", get_path(), get_guid_as_string())); } void FmodEventDescription::load_sample_data() { ERROR_CHECK_WITH_REASON(_wrapped->loadSampleData(), vformat("Cannot load sample data for event %s with guid %s", get_path(), get_guid_as_string())); } void FmodEventDescription::unload_sample_data() { ERROR_CHECK_WITH_REASON(_wrapped->unloadSampleData(), vformat("Cannot unload sample data for event %s with guid %s", get_path(), get_guid_as_string())); } int FmodEventDescription::get_sample_loading_state() { FMOD_STUDIO_LOADING_STATE s; ERROR_CHECK_WITH_REASON(_wrapped->getSampleLoadingState(&s), vformat("Cannot get sample loading state for event %s with guid %s", get_path(), get_guid_as_string())); return s; } bool FmodEventDescription::is_3d() { bool is3D = false; ERROR_CHECK_WITH_REASON(_wrapped->is3D(&is3D), vformat("Cannot check is_3d for event %s with guid %s", get_path(), get_guid_as_string())); return is3D; } bool FmodEventDescription::is_one_shot() { bool isOneShot = false; ERROR_CHECK_WITH_REASON(_wrapped->isOneshot(&isOneShot), vformat("Cannot check is_one_shot for event %s with guid %s", get_path(), get_guid_as_string())); return isOneShot; } bool FmodEventDescription::is_snapshot() { bool isSnapshot = false; ERROR_CHECK_WITH_REASON(_wrapped->isSnapshot(&isSnapshot), vformat("Cannot check is_snapshot for event %s with guid %s", get_path(), get_guid_as_string())); return isSnapshot; } bool FmodEventDescription::is_stream() { bool isStream = false; ERROR_CHECK_WITH_REASON(_wrapped->isStream(&isStream), vformat("Cannot check is_stream for event %s with guid %s", get_path(), get_guid_as_string())); return isStream; } bool FmodEventDescription::has_sustain_point() { bool hasSustainPoint = false; ERROR_CHECK_WITH_REASON(_wrapped->hasSustainPoint(&hasSustainPoint), vformat("Cannot check has_sustain_point for event %s with guid %s", get_path(), get_guid_as_string())); return hasSustainPoint; } Array FmodEventDescription::get_min_max_distance() { float minDistance; float maxDistance; Array ret; ERROR_CHECK_WITH_REASON(_wrapped->getMinMaxDistance(&minDistance, &maxDistance), vformat("Cannot get min max distance for event %s with guid %s", get_path(), get_guid_as_string())); ret.append(minDistance); ret.append(maxDistance); return ret; } float FmodEventDescription::get_sound_size() { float soundSize = 0.f; ERROR_CHECK_WITH_REASON(_wrapped->getSoundSize(&soundSize), vformat("Cannot get sound size for event %s with guid %s", get_path(), get_guid_as_string())); return soundSize; } Ref FmodEventDescription::get_parameter_by_name(const String& name) const { Ref param_desc; FMOD_STUDIO_PARAMETER_DESCRIPTION fmod_desc; if (ERROR_CHECK_WITH_REASON(_wrapped->getParameterDescriptionByName(name.utf8().get_data(), &fmod_desc), vformat("Cannot get parameter %s description for event %s with guid %s", name, get_path(), get_guid_as_string()))) { param_desc = FmodParameterDescription::create_ref(fmod_desc); } return param_desc; } Ref FmodEventDescription::get_parameter_by_id(uint64_t id) const { Ref param_desc; FMOD_STUDIO_PARAMETER_ID param_id { ulong_to_fmod_parameter_id(id) }; FMOD_STUDIO_PARAMETER_DESCRIPTION fmod_desc; if (ERROR_CHECK_WITH_REASON(_wrapped->getParameterDescriptionByID(param_id, &fmod_desc), vformat("Cannot get parameter %d description for event %s with guid %s", id, get_path(), get_guid_as_string()))) { param_desc = FmodParameterDescription::create_ref(fmod_desc); } return param_desc; } int FmodEventDescription::get_parameter_count() const { int count = 0; ERROR_CHECK_WITH_REASON(_wrapped->getParameterDescriptionCount(&count), vformat("Cannot get parameter count for event %s with guid %s", get_path(), get_guid_as_string())); return count; } Ref FmodEventDescription::get_parameter_by_index(int index) const { Ref param_desc; FMOD_STUDIO_PARAMETER_DESCRIPTION fmod_desc; if (ERROR_CHECK_WITH_REASON(_wrapped->getParameterDescriptionByIndex(index, &fmod_desc), vformat("Cannot get parameter with index %d for event %s with guid %s", index, get_path(), get_guid_as_string()))) { param_desc = FmodParameterDescription::create_ref(fmod_desc); } return param_desc; } Array FmodEventDescription::get_parameters() const { Array parameters; for (int i = 0; i < get_parameter_count(); ++i) { parameters.append(get_parameter_by_index(i)); } return parameters; } String FmodEventDescription::get_parameter_label_by_id(uint64_t id, int label_index) const { char label[PARAMETER_LABEL_BUFFER_SIZE]; int retrieved; _wrapped->getParameterLabelByID( ulong_to_fmod_parameter_id(id), label_index, label, PARAMETER_LABEL_BUFFER_SIZE, &retrieved ); return {label}; } String FmodEventDescription::get_parameter_label_by_name(const String& parameter_name, int label_index) const { char label[PARAMETER_LABEL_BUFFER_SIZE]; int retrieved; _wrapped->getParameterLabelByName( parameter_name.utf8().get_data(), label_index, label, PARAMETER_LABEL_BUFFER_SIZE, &retrieved ); return {label}; } String FmodEventDescription::get_parameter_label_by_index(int index, int label_index) const { char label[PARAMETER_LABEL_BUFFER_SIZE]; int retrieved; _wrapped->getParameterLabelByIndex( index, label_index, label, PARAMETER_LABEL_BUFFER_SIZE, &retrieved ); return {label}; } PackedStringArray FmodEventDescription::get_parameter_labels_by_id(uint64_t id) const { PackedStringArray labels; Ref parameter {get_parameter_by_id(id)}; if (!parameter->is_labeled()) { return labels; } for (int i = 0; i <= static_cast(parameter->get_maximum()); ++i) { labels.append(get_parameter_label_by_id(id, i)); } return labels; } PackedStringArray FmodEventDescription::get_parameter_labels_by_name(const String& parameter_name) const { PackedStringArray labels; Ref parameter {get_parameter_by_name(parameter_name)}; if (!parameter->is_labeled()) { return labels; } for (int i = 0; i <= static_cast(parameter->get_maximum()); ++i) { labels.append(get_parameter_label_by_name(parameter_name, i)); } return labels; } PackedStringArray FmodEventDescription::get_parameter_labels_by_index(int index) const { PackedStringArray labels; Ref parameter {get_parameter_by_index(index)}; if (!parameter->is_labeled()) { return labels; } for (int i = 0; i <= static_cast(parameter->get_maximum()); ++i) { labels.append(get_parameter_label_by_index(index, i)); } return labels; } Dictionary FmodEventDescription::get_user_property(const String& name) { Dictionary propDesc; FMOD_STUDIO_USER_PROPERTY uProp; if (ERROR_CHECK_WITH_REASON(_wrapped->getUserProperty(name.utf8().get_data(), &uProp), vformat("Cannot get user property %s for event %s with guid %s", name, get_path(), get_guid_as_string()))) { FMOD_STUDIO_USER_PROPERTY_TYPE fType = uProp.type; if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_INTEGER) propDesc[String(uProp.name)] = uProp.intvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_BOOLEAN) propDesc[String(uProp.name)] = (bool) uProp.boolvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_FLOAT) propDesc[String(uProp.name)] = uProp.floatvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_STRING) propDesc[String(uProp.name)] = String(uProp.stringvalue); } return propDesc; } int FmodEventDescription::get_user_property_count() { int count = 0; ERROR_CHECK_WITH_REASON(_wrapped->getUserPropertyCount(&count), vformat("Cannot get user property count for event %s with guid %s", get_path(), get_guid_as_string())); return count; } Dictionary FmodEventDescription::user_property_by_index(int index) { Dictionary propDesc; FMOD_STUDIO_USER_PROPERTY uProp; if (ERROR_CHECK_WITH_REASON(_wrapped->getUserPropertyByIndex(index, &uProp), vformat("Cannot get user property with index %d for event %s with guid %s", index, get_path(), get_guid_as_string()))) { FMOD_STUDIO_USER_PROPERTY_TYPE fType = uProp.type; if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_INTEGER) propDesc[String(uProp.name)] = uProp.intvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_BOOLEAN) propDesc[String(uProp.name)] = (bool) uProp.boolvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_FLOAT) propDesc[String(uProp.name)] = uProp.floatvalue; else if (fType == FMOD_STUDIO_USER_PROPERTY_TYPE_STRING) propDesc[String(uProp.name)] = String(uProp.stringvalue); } return propDesc; } ================================================ FILE: src/studio/fmod_event_description.h ================================================ #ifndef GODOTFMOD_FMOD_EVENT_DESCRIPTION_H #define GODOTFMOD_FMOD_EVENT_DESCRIPTION_H #include "classes/ref_counted.hpp" #include "fmod_parameter_description.h" #include "fmod_studio.hpp" #include "helpers/common.h" namespace godot { class FmodEventDescription : public RefCounted { FMODCLASSWITHPATH(FmodEventDescription, RefCounted, FMOD::Studio::EventDescription); public: FmodEventDescription() = default; ~FmodEventDescription() override = default; int get_length(); Array get_instance_list(); int get_instance_count(); void release_all_instances(); void load_sample_data(); void unload_sample_data(); int get_sample_loading_state(); bool is_3d(); bool is_one_shot(); bool is_snapshot(); bool is_stream(); bool has_sustain_point(); Array get_min_max_distance(); float get_sound_size(); Ref get_parameter_by_name(const String& name) const; Ref get_parameter_by_id(uint64_t id) const; int get_parameter_count() const; Ref get_parameter_by_index(int index) const; Array get_parameters() const; String get_parameter_label_by_id(uint64_t id, int label_index) const; String get_parameter_label_by_name(const String& parameter_name, int label_index) const; String get_parameter_label_by_index(int index, int label_index) const; PackedStringArray get_parameter_labels_by_id(uint64_t id) const; PackedStringArray get_parameter_labels_by_name(const String& parameter_name) const; PackedStringArray get_parameter_labels_by_index(int index) const; Dictionary get_user_property(const String& name); int get_user_property_count(); Dictionary user_property_by_index(int index); protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_EVENT_DESCRIPTION_H ================================================ FILE: src/studio/fmod_parameter_description.cpp ================================================ #include "fmod_parameter_description.h" #include using namespace godot; const String& FmodParameterDescription::get_name() const { return _name; } uint64_t FmodParameterDescription::get_id() const { return fmod_parameter_id_to_ulong(_wrapped.id); } float FmodParameterDescription::get_minimum() const { return _wrapped.minimum; } float FmodParameterDescription::get_maximum() const { return _wrapped.maximum; } float FmodParameterDescription::get_default_value() const { return _wrapped.defaultvalue; } bool FmodParameterDescription::is_read_only() const { return (_wrapped.flags & FMOD_STUDIO_PARAMETER_READONLY) == FMOD_STUDIO_PARAMETER_READONLY; } bool FmodParameterDescription::is_automatic() const { return (_wrapped.flags & FMOD_STUDIO_PARAMETER_AUTOMATIC) == FMOD_STUDIO_PARAMETER_AUTOMATIC; } bool FmodParameterDescription::is_global() const { return (_wrapped.flags & FMOD_STUDIO_PARAMETER_GLOBAL) == FMOD_STUDIO_PARAMETER_GLOBAL; } bool FmodParameterDescription::is_discrete() const { return (_wrapped.flags & FMOD_STUDIO_PARAMETER_DISCRETE) == FMOD_STUDIO_PARAMETER_DISCRETE; } bool FmodParameterDescription::is_labeled() const { return (_wrapped.flags & FMOD_STUDIO_PARAMETER_LABELED) == FMOD_STUDIO_PARAMETER_LABELED; } void FmodParameterDescription::_bind_methods() { ClassDB::bind_method(D_METHOD("get_name"), &FmodParameterDescription::get_name); ClassDB::bind_method(D_METHOD("get_id"), &FmodParameterDescription::get_id); ClassDB::bind_method(D_METHOD("get_minimum"), &FmodParameterDescription::get_minimum); ClassDB::bind_method(D_METHOD("get_maximum"), &FmodParameterDescription::get_maximum); ClassDB::bind_method(D_METHOD("get_default_value"), &FmodParameterDescription::get_default_value); ClassDB::bind_method(D_METHOD("is_read_only"), &FmodParameterDescription::is_read_only); ClassDB::bind_method(D_METHOD("is_automatic"), &FmodParameterDescription::is_automatic); ClassDB::bind_method(D_METHOD("is_global"), &FmodParameterDescription::is_global); ClassDB::bind_method(D_METHOD("is_discrete"), &FmodParameterDescription::is_discrete); ClassDB::bind_method(D_METHOD("is_labeled"), &FmodParameterDescription::is_labeled); } ================================================ FILE: src/studio/fmod_parameter_description.h ================================================ #ifndef GODOTFMOD_FMOD_PARAMETER_DESCRIPTION_H #define GODOTFMOD_FMOD_PARAMETER_DESCRIPTION_H #include "fmod_studio_common.h" #include namespace godot { class FmodParameterDescription : public RefCounted { GDCLASS(FmodParameterDescription, RefCounted) String _name; FMOD_STUDIO_PARAMETER_DESCRIPTION _wrapped; public: const String& get_name() const; uint64_t get_id() const; float get_minimum() const; float get_maximum() const; float get_default_value() const; bool is_read_only() const; bool is_automatic() const; bool is_global() const; bool is_discrete() const; bool is_labeled() const; inline static Ref create_ref(const FMOD_STUDIO_PARAMETER_DESCRIPTION& wrapped) { Ref ref; ref.instantiate(); ref->_wrapped = wrapped; ref->_name = wrapped.name; return ref; } protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_PARAMETER_DESCRIPTION_H ================================================ FILE: src/studio/fmod_vca.cpp ================================================ #include "fmod_vca.h" #include "helpers/common.h" using namespace godot; void FmodVCA::_bind_methods() { ClassDB::bind_method(D_METHOD("get_volume"), &FmodVCA::get_volume); ClassDB::bind_method(D_METHOD("set_volume", "volume"), &FmodVCA::set_volume); ClassDB::bind_method(D_METHOD("is_valid"), &FmodVCA::is_valid); ClassDB::bind_method(D_METHOD("get_path"), &FmodVCA::get_path); ClassDB::bind_method(D_METHOD("get_guid"), &FmodVCA::get_guid_as_string); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume",PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_volume", "get_volume"); } float FmodVCA::get_volume() { float volume = 0.0f; ERROR_CHECK_WITH_REASON(_wrapped->getVolume(&volume), vformat("Cannot get VCA %s volume.", get_path())); return volume; } void FmodVCA::set_volume(float volume) { ERROR_CHECK_WITH_REASON(_wrapped->setVolume(volume), vformat("Cannot set VCA %s volume to %f", get_path(), volume)); } ================================================ FILE: src/studio/fmod_vca.h ================================================ #ifndef GODOTFMOD_FMOD_VCA_H #define GODOTFMOD_FMOD_VCA_H #include "classes/ref_counted.hpp" #include "fmod_studio.hpp" #include "helpers/common.h" namespace godot { class FmodVCA : public RefCounted { FMODCLASSWITHPATH(FmodVCA, RefCounted, FMOD::Studio::VCA); public: FmodVCA() = default; ~FmodVCA() override = default; float get_volume(); void set_volume(float volume); protected: static void _bind_methods(); }; }// namespace godot #endif// GODOTFMOD_FMOD_VCA_H ================================================ FILE: src/tools/fmod_editor_export_plugin.cpp ================================================ #ifdef TOOLS_ENABLED #include "fmod_editor_export_plugin.h" #include "fmod_server.h" #include #include #include using namespace godot; constexpr const char* FMOD_FILE_EXTENSIONS[4] {".bank", ".ogg", ".mp3", ".wav"}; constexpr const char* ANDROID_BUILD_DIRS[2] = { "res://android/build", "res:///android/build" }; constexpr const char* FMOD_AUTO_EXPORT_BANKS_SETTINGS_KEY = "fmod/auto_export_banks"; void FmodEditorExportPlugin::_export_begin(const PackedStringArray& features, bool is_debug, const String& path, uint32_t flags) { if (get_option(FMOD_AUTO_EXPORT_BANKS_SETTINGS_KEY) != Variant(false)) { PackedStringArray excluded_folders; for (const char* dir : ANDROID_BUILD_DIRS) { excluded_folders.append(dir); } for (const char* extension : FMOD_FILE_EXTENSIONS) { PackedStringArray files; list_files_in_folder(files, "res://", extension, excluded_folders); for (const String& file : files) { GODOT_LOG_VERBOSE(vformat("Adding %s to pck", file)); add_file(file, FileAccess::get_file_as_bytes(file), false); } } } bool is_windows_export = features.has("windows"); bool is_linux_export = features.has("linux"); bool is_macos_export = features.has("macos"); bool is_ios_export = features.has("ios"); bool is_android_export = features.has("android"); Ref plugins_settings = FmodPluginsSettings::get_from_project_settings(); if (is_macos_export) { PackedStringArray plugins_libraries_path = _get_libraries_to_export(plugins_settings, "macos", ".dylib"); for (const String& library_path : plugins_libraries_path) { add_macos_plugin_file(library_path); } } else if (is_linux_export || is_windows_export) { String target_dir = path.get_base_dir(); String os_name = is_linux_export ? "linux" : "windows"; String extension = is_linux_export ? ".so" : ".dll"; PackedStringArray plugins_libraries_path = _get_libraries_to_export(plugins_settings, os_name, extension); Ref dir_access = DirAccess::open(target_dir); if (dir_access.is_null()) { GODOT_LOG_ERROR(vformat("Failed to open target directory: %s", target_dir)); return; } for (const String& library_path : plugins_libraries_path) { GODOT_LOG_INFO(vformat("Will copy %s to %s", library_path, target_dir)); Ref file_access = FileAccess::open(library_path, FileAccess::READ); if (file_access.is_null()) { GODOT_LOG_ERROR(vformat("Failed to open library file: %s", library_path)); continue; } dir_access->copy(library_path, target_dir.path_join(file_access->get_path().get_file())); } } else if (is_ios_export) { PackedStringArray plugins_libraries_path = _get_libraries_to_export(plugins_settings, "ios", ".a"); for (const String& library_path : plugins_libraries_path) { add_ios_project_static_lib(library_path); } String cpp_code_declaration = R"( #include #include struct FMOD_DSP_DESCRIPTION; struct FMOD_CODEC_DESCRIPTION; struct FMOD_OUTPUT_DESCRIPTION; typedef void* FMOD_SYSTEM_PTR; typedef uint32_t (*REGISTER_DSP_METHOD)(FMOD_SYSTEM_PTR system, FMOD_DSP_DESCRIPTION* description, uint32_t* handle); typedef uint32_t (*REGISTER_CODEC_METHOD)(FMOD_SYSTEM_PTR system, FMOD_CODEC_DESCRIPTION* description, uint32_t* handle); typedef uint32_t (*REGISTER_OUTPUT_METHOD)(FMOD_SYSTEM_PTR system, FMOD_OUTPUT_DESCRIPTION* description, uint32_t* handle); typedef struct { FMOD_SYSTEM_PTR system; REGISTER_DSP_METHOD register_dsp_method; REGISTER_CODEC_METHOD register_codec_method; REGISTER_OUTPUT_METHOD register_output_method; } FMOD_IOS_INTERFACE; )"; String cpp_code_external_plugin_declaration = "extern \"C\" {\n"; String cpp_code_load_method = R"( extern "C" __attribute__((visibility("default"))) __attribute__((used)) uint32_t* load_all_fmod_plugins(FMOD_IOS_INTERFACE* p_interface, uint32_t* r_count) { FMOD_SYSTEM_PTR fmod_system = p_interface->system; uint32_t handle; uint32_t* handles = reinterpret_cast(std::malloc(sizeof(uint32_t) * %s)); )"; const Array& plugin_methods = plugins_settings->get_static_plugins_methods(); int64_t method_count = plugin_methods.size(); for (int i = 0; i < method_count; ++i) { const Ref& plugin_method = plugin_methods[i]; const String& method_name = plugin_method->get_method_name(); switch (plugin_method->get_type()) { case FmodStaticPluginMethod::CODEC: cpp_code_external_plugin_declaration += vformat(" FMOD_CODEC_DESCRIPTION* %s();\n", method_name); cpp_code_load_method += vformat(" p_interface->register_codec_method(fmod_system, %s(), &handle);\n", method_name); cpp_code_load_method += vformat(" handles[%s] = handle;\n", i); break; case FmodStaticPluginMethod::DSP: cpp_code_external_plugin_declaration += vformat(" FMOD_DSP_DESCRIPTION* %s();\n", method_name); cpp_code_load_method += vformat(" p_interface->register_dsp_method(fmod_system, %s(), &handle);\n", method_name); cpp_code_load_method += vformat(" handles[%s] = handle;\n", i); break; case FmodStaticPluginMethod::OUTPUT: cpp_code_external_plugin_declaration += vformat(" FMOD_OUTPUT_DESCRIPTION* %s();\n", method_name); cpp_code_load_method += vformat(" p_interface->register_output_method(fmod_system, %s(), &handle);\n", method_name); cpp_code_load_method += vformat(" handles[%s] = handle;\n", i); break; case FmodStaticPluginMethod::COUNT: break; } } cpp_code_external_plugin_declaration += "}\n"; cpp_code_load_method += R"( *r_count = %s; return handles; } )"; add_ios_cpp_code( vformat( "%s%s%s", cpp_code_declaration, cpp_code_external_plugin_declaration, vformat(cpp_code_load_method, method_count, method_count) ) ); } else if (is_android_export) { bool is_x86 = features.has("x86_64"); bool is_arm64 = features.has("arm64"); if (is_x86) { PackedStringArray plugins_libraries_path = _get_libraries_to_export(plugins_settings, "android", ".so", "x86_64"); PackedStringArray tags; tags.append("x86_64"); for (const String& library_path : plugins_libraries_path) { add_shared_object(library_path, tags, String()); } } if (is_arm64) { PackedStringArray plugins_libraries_path = _get_libraries_to_export(plugins_settings, "android", ".so", "arm64"); PackedStringArray tags; tags.append("arm64-v8a"); for (const String& library_path : plugins_libraries_path) { add_shared_object(library_path, tags, String()); } } } } String FmodEditorExportPlugin::_get_name() const { return "FmodEditorExportPlugin"; } TypedArray FmodEditorExportPlugin::_get_export_options(const Ref& platform) const { TypedArray options; { Dictionary option_dict; Dictionary option; option["name"] = FMOD_AUTO_EXPORT_BANKS_SETTINGS_KEY; option["type"] = Variant::BOOL; option_dict["option"] = option; option_dict["default_value"] = true; option_dict["update_visibility"] = true; options.append(option_dict); } return options; } void FmodEditorExportPlugin::_bind_methods() {} PackedStringArray FmodEditorExportPlugin::_get_libraries_to_export(const Ref& settings, const String& p_os_name, const String& p_extension, const String& p_arch) { PackedStringArray result; if (settings.is_null() || settings->get_plugins_base_path().is_empty()) { return result; } String plugins_libraries_path = get_plugins_os_directory(settings, p_os_name, p_arch); list_files_in_folder(result, plugins_libraries_path, p_extension); return result; } #endif ================================================ FILE: src/tools/fmod_editor_export_plugin.h ================================================ #ifdef TOOLS_ENABLED #ifndef GODOTFMOD_FMOD_EDITOR_EXPORT_PLUGIN_H #define GODOTFMOD_FMOD_EDITOR_EXPORT_PLUGIN_H #include #include #include namespace godot { class FmodEditorExportPlugin : public EditorExportPlugin { GDCLASS(FmodEditorExportPlugin, EditorExportPlugin) public: void _export_begin(const PackedStringArray &features, bool is_debug, const String &path, uint32_t flags) override; String _get_name() const override; virtual TypedArray _get_export_options(const Ref& platform) const override; static void _bind_methods(); FmodEditorExportPlugin() = default; ~FmodEditorExportPlugin() = default; private: static PackedStringArray _get_libraries_to_export(const Ref& settings, const String& p_os_name, const String& p_extension, const String& p_arch = ""); }; } #endif// GODOTFMOD_FMOD_EDITOR_EXPORT_PLUGIN_H #endif ================================================ FILE: src/tools/fmod_editor_plugin.cpp ================================================ #ifdef TOOLS_ENABLED #include "fmod_editor_plugin.h" #include "fmod_editor_export_plugin.h" #include "classes/os.hpp" #include "resources/fmod_plugins_settings.h" #include #include #include #include #include #include #include #include #include #include using namespace godot; void FmodEditorPlugin::_ready() { add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FMOD_SETTING_AUTO_INITIALIZE), DEFAULT_AUTO_INITIALIZE, Variant::Type::BOOL ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::CHANNEL_COUNT_OPTION), FmodGeneralSettings::DEFAULT_CHANNEL_COUNT, Variant::Type::INT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::IS_LIVE_UPDATE_ENABLED_OPTION), FmodGeneralSettings::DEFAULT_IS_LIVEUPDATE, Variant::Type::BOOL ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::IS_LIVE_MEMORY_TRACKING_ENABLED_OPTION), FmodGeneralSettings::DEFAULT_IS_MEMORY_TRACKING, Variant::Type::BOOL ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::SOFTWARE_FORMAT_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::SAMPLE_RATE_OPTION), FmodSoftwareFormatSettings::DEFAULT_SAMPLE_RATE, Variant::Type::INT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::SOFTWARE_FORMAT_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::SPEAKER_MODE_OPTION), FmodSoftwareFormatSettings::DEFAULT_SPEAKER_MODE, Variant::Type::INT, PROPERTY_HINT_ENUM, "DEFAULT,RAW,MONO,STEREO,QUAD,SURROUND,5POINT1,7POINT1,7POINT1POINT4" ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::SOFTWARE_FORMAT_SETTINGS_BASE_PATH, FmodSoftwareFormatSettings::RAW_SPEAKER_COUNT_OPTION), FmodSoftwareFormatSettings::DEFAULT_RAW_SPEAKER_COUNT, Variant::Type::INT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::DEFAULT_LISTENER_COUNT_OPTION), FmodGeneralSettings::DEFAULT_DEFAULT_LISTENER_COUNT, Variant::Type::INT ); String bank_path_option_name = vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::BANKS_PATH_OPTION); add_setting(bank_path_option_name, FmodGeneralSettings::DEFAULT_BANKS_PATH, Variant::Type::STRING, PROPERTY_HINT_DIR ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodPluginsSettings::PLUGINS_SETTINGS_BASE_PATH, FmodPluginsSettings::RESOURCE_OPTION), FmodPluginsSettings::DEFAULT_RESOURCE_OPTION, Variant::Type::STRING, PROPERTY_HINT_FILE ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodGeneralSettings::INITIALIZE_BASE_PATH, FmodGeneralSettings::SHOULD_LOAD_BY_NAME), FmodGeneralSettings::DEFAULT_SHOULD_LOAD_BY_NAME, Variant::Type::BOOL ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodDspSettings::DSP_SETTINGS_BASE_PATH, FmodDspSettings::DSP_BUFFER_SIZE_OPTION), FmodDspSettings::DEFAULT_DSP_BUFFER_SIZE, Variant::Type::INT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodDspSettings::DSP_SETTINGS_BASE_PATH, FmodDspSettings::DSP_BUFFER_COUNT_OPTION), FmodDspSettings::DEFAULT_DSP_BUFFER_COUNT, Variant::Type::INT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSound3DSettings::THREE_D_SETTINGS_BASE_PATH, FmodSound3DSettings::DOPPLER_SCALE_OPTION), FmodSound3DSettings::DEFAULT_DOPPLER_SCALE, Variant::Type::FLOAT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSound3DSettings::THREE_D_SETTINGS_BASE_PATH, FmodSound3DSettings::DISTANCE_FACTOR_OPTION), FmodSound3DSettings::DEFAULT_DISTANCE_FACTOR, Variant::Type::FLOAT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodSound3DSettings::THREE_D_SETTINGS_BASE_PATH, FmodSound3DSettings::ROLLOFF_SCALE_OPTION), FmodSound3DSettings::DEFAULT_ROLLOFF_SCALE, Variant::Type::FLOAT ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodLoggingSettings::LOGGING_SETTINGS_BASE_PATH, FmodLoggingSettings::DEBUG_LEVEL_OPTION), FmodLoggingSettings::DEFAULT_DEBUG_LEVEL, Variant::Type::INT, PROPERTY_HINT_ENUM, "Inherit,None,Error,Warning,Log,Verbose" ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodLoggingSettings::LOGGING_SETTINGS_BASE_PATH, FmodLoggingSettings::LOG_OUTPUT_OPTION), FmodLoggingSettings::DEFAULT_LOG_OUTPUT, Variant::Type::INT, PROPERTY_HINT_ENUM, "TTY,File,Godot" ); add_setting( vformat("%s/%s/%s", FMOD_SETTINGS_BASE_PATH, FmodLoggingSettings::LOGGING_SETTINGS_BASE_PATH, FmodLoggingSettings::LOG_FILE_PATH_OPTION), FmodLoggingSettings::DEFAULT_LOG_FILE_PATH, Variant::Type::STRING, PROPERTY_HINT_FILE, "*.txt,*.log" ); } void FmodEditorPlugin::add_setting( const String& p_name, const Variant& p_default_value, Variant::Type p_type, PropertyHint p_hint, const String& p_hint_string ) { Dictionary setting; setting["name"] = p_name; setting["type"] = p_type; setting["hint"] = p_hint; setting["hint_string"] = p_hint_string; if (!ProjectSettings::get_singleton()->has_setting(p_name)) { ProjectSettings::get_singleton()->set_setting(p_name, p_default_value); } ProjectSettings::get_singleton()->add_property_info(setting); ProjectSettings::get_singleton()->set_as_basic(p_name, true); ProjectSettings::get_singleton()->set_initial_value(p_name, p_default_value); } void FmodEditorPlugin::_bind_methods() {} #endif ================================================ FILE: src/tools/fmod_editor_plugin.h ================================================ #ifdef TOOLS_ENABLED #ifndef GODOTFMOD_FMOD_EDITOR_PLUGIN_H #define GODOTFMOD_FMOD_EDITOR_PLUGIN_H #include "fmod_editor_export_plugin.h" #include "studio/fmod_bank.h" #include namespace godot { class FmodEditorPlugin : public EditorPlugin { GDCLASS(FmodEditorPlugin, EditorPlugin) public: void _ready() override; FmodEditorPlugin() = default; ~FmodEditorPlugin() = default; private: static void add_setting( const String& p_name, const Variant& p_default_value, Variant::Type p_type, PropertyHint p_hint = PROPERTY_HINT_NONE, const String& p_hint_string = "" ); protected: static void _bind_methods(); }; } #endif// GODOTFMOD_FMOD_EDITOR_PLUGIN_H #endif