Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d856fd937 | |||
| cc7cb90041 | |||
| 7a787a36d6 | |||
| bfa1f251dd | |||
| 673368a200 | |||
| c1108e96d7 | |||
| 15cb80d045 | |||
| 1d298b3080 | |||
| 42ff38f39b | |||
| dafb0c96cc | |||
| ef454e9502 | |||
| cc70fb361b | |||
| 7bf19868e7 | |||
| d1f83525b1 | |||
| 4bcbda9690 | |||
| e51ef5a517 | |||
| 50de6abb5d | |||
| 95616f61fc | |||
| b15a4fef95 | |||
| 14d29d68bb | |||
| 9d612682ec | |||
| 9bfe37af62 | |||
| 55eba7fcc8 | |||
| 7a3e61b86f | |||
| 8153ec07e7 | |||
| ddc85655be | |||
| c92eb19a1c | |||
| 290f79afd4 | |||
| 5408f455af | |||
| 3a21f00528 | |||
| 22e8c27878 | |||
| 6c4454848a | |||
| 175e67d2d6 | |||
| ab69fa9323 | |||
| 263990b086 | |||
| 5da2aa31ab | |||
| 4f64139d61 | |||
| de41bbeb8d | |||
| a4873f183c | |||
| d37ae8d26c | |||
| 5227fedf15 | |||
| c9738d9c61 |
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
env:
|
||||
GODOT_VERSION: 4.6
|
||||
DOTNET_VERSION: 'net9.0'
|
||||
GAME_NAME: MovementTests
|
||||
ITCHIO_USERNAME: Minimata
|
||||
ITCHIO_GAMEID: MovementTests
|
||||
@@ -38,35 +39,52 @@ jobs:
|
||||
INITIAL_VERSION: 0.1.0
|
||||
DEFAULT_BUMP: patch
|
||||
|
||||
Test:
|
||||
runs-on: godot
|
||||
# env:
|
||||
# RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
|
||||
steps:
|
||||
- name: Checkout with LFS
|
||||
uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
|
||||
|
||||
- name: Run tests
|
||||
uses: godot-gdunit-labs/gdUnit4-action@v1
|
||||
with:
|
||||
godot-version: ${GODOT_VERSION}
|
||||
godot-net: true
|
||||
godot-force-mono: true
|
||||
dotnet-version: 'net9.0'
|
||||
paths: |
|
||||
res://tests/
|
||||
publish-report: false
|
||||
upload-report: false
|
||||
arguments: "--verbose --headless --import"
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v3-node20
|
||||
with:
|
||||
name: Test Report
|
||||
path: ${{ github.workspace }}/reports/test-result.html
|
||||
# Test:
|
||||
# runs-on: godot
|
||||
## env:
|
||||
## RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
|
||||
# steps:
|
||||
# - name: Checkout with LFS
|
||||
# uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
|
||||
#
|
||||
# - name: Setup Godot
|
||||
# id: setup-godot
|
||||
# uses: https://git.game-dev.space/minimata/setup-godot.git@main
|
||||
# with:
|
||||
# godot-version: ${GODOT_VERSION}
|
||||
# dotnet-version: ${DOTNET_VERSION}
|
||||
#
|
||||
# - name: Run C# Tests
|
||||
# env:
|
||||
# GODOT_BIN: ${{ steps.setup-godot.outputs.godot_bin }}
|
||||
# shell: bash
|
||||
# run: |
|
||||
# dotnet test --no-build --settings .runsettings --results-directory ./reports --logger "console;verbosity=normal" --logger "trx;LogFileName=results.xml" -- GdUnit4.Parameters="--verbose --headless --import"
|
||||
#
|
||||
## - name: Run tests
|
||||
## uses: godot-gdunit-labs/gdUnit4-action@v1
|
||||
## with:
|
||||
## godot-version: ${GODOT_VERSION}
|
||||
## godot-net: true
|
||||
## godot-force-mono: true
|
||||
## dotnet-version: ${DOTNET_VERSION}
|
||||
## paths: |
|
||||
## res://tests/
|
||||
## publish-report: false
|
||||
## upload-report: false
|
||||
## console-verbosity: 'normal'
|
||||
## arguments: "--verbose --headless --import"
|
||||
#
|
||||
# - name: Upload test report
|
||||
# uses: actions/upload-artifact@v3-node20
|
||||
# with:
|
||||
# name: Test Report
|
||||
# path: ${{ github.workspace }}/reports/test-result.html
|
||||
|
||||
Export:
|
||||
runs-on: godot
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
|
||||
needs:
|
||||
- BumpTag
|
||||
|
||||
@@ -78,8 +96,8 @@ jobs:
|
||||
id: setup-godot
|
||||
uses: https://git.game-dev.space/minimata/setup-godot.git@main
|
||||
with:
|
||||
godot-version: '4.6'
|
||||
dotnet-version: 'net9.0'
|
||||
godot-version: ${GODOT_VERSION}
|
||||
dotnet-version: ${DOTNET_VERSION}
|
||||
|
||||
- name: Remove GDUnit addon
|
||||
run: |
|
||||
@@ -89,6 +107,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -v -p build/windows
|
||||
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --export-release "Windows Desktop" build/windows/${{ env.GAME_NAME }}.exe
|
||||
ls -la build/windows
|
||||
|
||||
# - name: Setup Butler
|
||||
# shell: bash
|
||||
|
||||
@@ -63,6 +63,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -v -p build/windows
|
||||
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --build-solutions --export-release "Windows Desktop" build/windows/${{ env.GAME_NAME }}.exe
|
||||
ls -la build/windows
|
||||
zip -r Windows.zip build/windows
|
||||
- name: Upload Windows to itch.io
|
||||
shell: bash
|
||||
@@ -79,7 +80,7 @@ jobs:
|
||||
mkdir -v -p build/windowsArm
|
||||
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --build-solutions --export-release "Windows ARM" build/windowsArm/${{ env.GAME_NAME }}.exe
|
||||
zip -r WindowsArm.zip build/windowsArm
|
||||
- name: Upload Windows to itch.io
|
||||
- name: Upload Windows ARM to itch.io
|
||||
shell: bash
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
@@ -94,7 +95,7 @@ jobs:
|
||||
mkdir -v -p build/linux
|
||||
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --export-release "Linux/X11" build/linux/${{ env.GAME_NAME }}.x86_64
|
||||
zip -r Linux.zip build/linux
|
||||
- name: Upload Windows to itch.io
|
||||
- name: Upload Linux to itch.io
|
||||
shell: bash
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
@@ -109,7 +110,7 @@ jobs:
|
||||
mkdir -v -p build/mac
|
||||
${{ steps.setup-godot.outputs.godot_bin }} --headless --verbose --export-release "macOS" build/mac/${{ env.GAME_NAME }}.zip
|
||||
zip -r Mac.zip build/mac
|
||||
- name: Upload Windows to itch.io
|
||||
- name: Upload Mac to itch.io
|
||||
shell: bash
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -15,4 +15,15 @@
|
||||
# Imported translations (automatically generated from CSV files)
|
||||
*.translation
|
||||
|
||||
.output.txt
|
||||
docs/legal/
|
||||
|
||||
.output.txt
|
||||
|
||||
*.suo
|
||||
*.user
|
||||
*.csproj.old*
|
||||
_ReSharper.*
|
||||
*.DotSettings.user
|
||||
bin
|
||||
obj
|
||||
packages
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.6.0">
|
||||
<Project Sdk="Godot.NET.Sdk/4.6.2">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
@@ -125,25 +125,14 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="addons\" />
|
||||
<Folder Include="tests\" />
|
||||
<Folder Include="tools\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RustyOptions" Version="0.10.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="addons/forge/Forge.props" />
|
||||
|
||||
<!-- XUnit -->
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- gdUnit4 package dependencies -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<RootNamespace>Movementtests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="export_presets.cfg" />
|
||||
<Content Include="menus\assets\git_logo\Git-Logo-2Color.png" />
|
||||
<Content Include="menus\assets\git_logo\Git-Logo-2Color.png.import" />
|
||||
<Content Include="menus\assets\git_logo\LICENSE.txt" />
|
||||
<Content Include="menus\assets\godot_engine_logo\LICENSE.txt" />
|
||||
<Content Include="menus\assets\godot_engine_logo\logo_vertical_color_dark.png" />
|
||||
<Content Include="menus\assets\godot_engine_logo\logo_vertical_color_dark.png.import" />
|
||||
<Content Include="menus\assets\icon.png" />
|
||||
<Content Include="menus\assets\icon.png.import" />
|
||||
<Content Include="menus\ATTRIBUTION.md" />
|
||||
<Content Include="menus\resources\themes\expedition.tres" />
|
||||
<Content Include="menus\resources\themes\gravity.tres" />
|
||||
<Content Include="menus\resources\themes\grow.tres" />
|
||||
<Content Include="menus\resources\themes\lab.tres" />
|
||||
<Content Include="menus\resources\themes\lore.tres" />
|
||||
<Content Include="menus\resources\themes\steal_this_theme.tres" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.gd" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.tscn" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.gd" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.tscn" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.gd" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\configurable_sub_viewport.gd" />
|
||||
<Content Include="menus\scenes\game_scene\configurable_sub_viewport.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\game_ui.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\input_display_label.gd" />
|
||||
<Content Include="menus\scenes\game_scene\input_display_label.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level.gd" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_1.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_2.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_3.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_1.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_2.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_3.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorial_manager.gd" />
|
||||
<Content Include="menus\scenes\game_scene\tutorial_manager.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\level_loading_screen.tscn" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.gd" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.tscn" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.gd" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.tscn" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.gd" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_extras_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu_with_mouse_sensitivity.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu_with_tabs.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu_with_extras.tscn" />
|
||||
<Content Include="menus\scenes\opening\opening.gd" />
|
||||
<Content Include="menus\scenes\opening\opening.gd.uid" />
|
||||
<Content Include="menus\scenes\opening\opening.tscn" />
|
||||
<Content Include="menus\scenes\opening\opening_with_logo.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\mini_options_overlaid_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.tscn" />
|
||||
<Content Include="menus\scripts\game_state.gd" />
|
||||
<Content Include="menus\scripts\game_state.gd.uid" />
|
||||
<Content Include="menus\scripts\level_list_and_state_manager.gd" />
|
||||
<Content Include="menus\scripts\level_list_and_state_manager.gd.uid" />
|
||||
<Content Include="menus\scripts\level_state.gd" />
|
||||
<Content Include="menus\scripts\level_state.gd.uid" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="addons\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RustyOptions" Version="0.10.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,142 +0,0 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.5.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<RootNamespace>Movementtests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include=".runsettings" />
|
||||
<Content Include="export_presets.cfg" />
|
||||
<Content Include="menus\assets\git_logo\Git-Logo-2Color.png" />
|
||||
<Content Include="menus\assets\git_logo\Git-Logo-2Color.png.import" />
|
||||
<Content Include="menus\assets\git_logo\LICENSE.txt" />
|
||||
<Content Include="menus\assets\godot_engine_logo\LICENSE.txt" />
|
||||
<Content Include="menus\assets\godot_engine_logo\logo_vertical_color_dark.png" />
|
||||
<Content Include="menus\assets\godot_engine_logo\logo_vertical_color_dark.png.import" />
|
||||
<Content Include="menus\assets\icon.png" />
|
||||
<Content Include="menus\assets\icon.png.import" />
|
||||
<Content Include="menus\ATTRIBUTION.md" />
|
||||
<Content Include="menus\resources\themes\expedition.tres" />
|
||||
<Content Include="menus\resources\themes\gravity.tres" />
|
||||
<Content Include="menus\resources\themes\grow.tres" />
|
||||
<Content Include="menus\resources\themes\lab.tres" />
|
||||
<Content Include="menus\resources\themes\lore.tres" />
|
||||
<Content Include="menus\resources\themes\steal_this_theme.tres" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.gd" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\credits\scrollable_credits.tscn" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.gd" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\credits\scrolling_credits.tscn" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.gd" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.gd.uid" />
|
||||
<Content Include="menus\scenes\end_credits\end_credits.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\configurable_sub_viewport.gd" />
|
||||
<Content Include="menus\scenes\game_scene\configurable_sub_viewport.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\game_ui.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\input_display_label.gd" />
|
||||
<Content Include="menus\scenes\game_scene\input_display_label.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level.gd" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level.gd.uid" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_1.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_2.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\levels\level_3.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_1.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_2.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorials\tutorial_3.tscn" />
|
||||
<Content Include="menus\scenes\game_scene\tutorial_manager.gd" />
|
||||
<Content Include="menus\scenes\game_scene\tutorial_manager.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\level_loading_screen.tscn" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.gd" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen.tscn" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.gd" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.gd.uid" />
|
||||
<Content Include="menus\scenes\loading_screen\loading_screen_with_shader_caching.tscn" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\level_select_menu\level_select_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.gd" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\main_menu\main_menu_with_animations.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_input_option_control.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\audio\audio_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\game_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\game\reset_game_control\reset_game_control.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_extras_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\input\input_options_menu_with_mouse_sensitivity.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\master_options_menu_with_tabs.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\mini_options_menu_with_reset.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.gd" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu.tscn" />
|
||||
<Content Include="menus\scenes\menus\options_menu\video\video_options_menu_with_extras.tscn" />
|
||||
<Content Include="menus\scenes\opening\opening.gd" />
|
||||
<Content Include="menus\scenes\opening\opening.gd.uid" />
|
||||
<Content Include="menus\scenes\opening\opening.tscn" />
|
||||
<Content Include="menus\scenes\opening\opening_with_logo.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\game_won_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_lost_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\level_won_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\mini_options_overlaid_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\overlaid_menu_container.tscn" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.gd" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.gd.uid" />
|
||||
<Content Include="menus\scenes\overlaid_menus\pause_menu.tscn" />
|
||||
<Content Include="menus\scripts\game_state.gd" />
|
||||
<Content Include="menus\scripts\game_state.gd.uid" />
|
||||
<Content Include="menus\scripts\level_list_and_state_manager.gd" />
|
||||
<Content Include="menus\scripts\level_list_and_state_manager.gd.uid" />
|
||||
<Content Include="menus\scripts\level_state.gd" />
|
||||
<Content Include="menus\scripts\level_state.gd.uid" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="addons\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RustyOptions" Version="0.10.1" />
|
||||
</ItemGroup>
|
||||
<!-- gdUnit4 package dependencies -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"/>
|
||||
<PackageReference Include="gdUnit4.api" Version="5.1.0-rc3"/>
|
||||
<PackageReference Include="gdUnit4.test.adapter" Version="3.0.0"/>
|
||||
<PackageReference Include="gdUnit4.analyzers" Version="1.0.0">
|
||||
<PrivateAssets>none</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
2
Movement tests.sln.DotSettings
Normal file
2
Movement tests.sln.DotSettings
Normal file
@@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeEditing/SuppressNullableWarningFix/Enabled/@EntryValue">False</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1,14 +0,0 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAction_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7c0f83388bfc4d2c9d09befcec9dd79bc90908_003Fb8_003F4d300c4d_003FAction_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAction_00602_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7c0f83388bfc4d2c9d09befcec9dd79bc90908_003F87_003Fded27e2d_003FAction_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnemy_005FScriptMethods_002Egenerated_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F8e71dc81611862c01a2cb998a1f327de14747655_003FEnemy_005FScriptMethods_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F716d154fef5cbe863cd637bd32beda6e3cec5f12e8fed2dc5b2d8149a0d558ab_003FNode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fdf73a4db74df89d59655c5fb6326406f47fbfa9af1fa81518fe0a07c49d34133_003FNode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASceneTree_002Ecs_002Fl_003AC_0021_003FUsers_003FMinimata_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F8d6960554e939a669841b1ece03d27df4ab42f92bb80be3767eaec8cdaccf84b_003FSceneTree_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=dd9a7ac6_002Dbb9b_002D4001_002Db145_002D15e6509b7e78/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<Solution />
|
||||
</SessionState></s:String>
|
||||
<s:String x:Key="/Default/Housekeeping/UnitTestingMru/UnitTestRunner/RunConfigurationFilename/@EntryValue">D:\Godot\Projects\movement-tests\.runsettings</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=floorplane/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -3,6 +3,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gamesmiths.Forge" Version="0.2.0" />
|
||||
<PackageReference Include="Gamesmiths.Forge" Version="0.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Editor;
|
||||
using Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
using Gamesmiths.Forge.Godot.Editor.Cues;
|
||||
using Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot;
|
||||
@@ -14,32 +19,25 @@ namespace Gamesmiths.Forge.Godot;
|
||||
public partial class ForgePluginLoader : EditorPlugin
|
||||
{
|
||||
private const string AutoloadPath = "uid://ba8fquhtwu5mu";
|
||||
private const string PluginScenePath = "uid://pjscvogl6jak";
|
||||
|
||||
private EditorDock? _editorDock;
|
||||
private PanelContainer? _dockedScene;
|
||||
private TagsEditorDock? _tagsEditorDock;
|
||||
private TagContainerInspectorPlugin? _tagContainerInspectorPlugin;
|
||||
private TagInspectorPlugin? _tagInspectorPlugin;
|
||||
private AttributeSetInspectorPlugin? _attributeSetInspectorPlugin;
|
||||
private CueHandlerInspectorPlugin? _cueHandlerInspectorPlugin;
|
||||
private AttributeEditorPlugin? _attributeEditorPlugin;
|
||||
private SharedVariableSetInspectorPlugin? _sharedVariableSetInspectorPlugin;
|
||||
private StatescriptGraphEditorDock? _statescriptGraphEditorDock;
|
||||
|
||||
private EditorFileSystem? _fileSystem;
|
||||
private Callable _resourcesReimportedCallable;
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
PackedScene pluginScene = ResourceLoader.Load<PackedScene>(PluginScenePath);
|
||||
EnsureForgeDataExists();
|
||||
|
||||
_editorDock = new EditorDock
|
||||
{
|
||||
Title = "Forge",
|
||||
DockIcon = GD.Load<Texture2D>("uid://cu6ncpuumjo20"),
|
||||
DefaultSlot = EditorDock.DockSlot.RightUl,
|
||||
};
|
||||
|
||||
_dockedScene = (PanelContainer)pluginScene.Instantiate();
|
||||
_dockedScene.GetNode<TagsEditor>("%Tags").IsPluginInstance = true;
|
||||
|
||||
_editorDock.AddChild(_dockedScene);
|
||||
AddDock(_editorDock);
|
||||
_tagsEditorDock = new TagsEditorDock();
|
||||
AddDock(_tagsEditorDock);
|
||||
|
||||
_tagContainerInspectorPlugin = new TagContainerInspectorPlugin();
|
||||
AddInspectorPlugin(_tagContainerInspectorPlugin);
|
||||
@@ -51,32 +49,89 @@ public partial class ForgePluginLoader : EditorPlugin
|
||||
AddInspectorPlugin(_cueHandlerInspectorPlugin);
|
||||
_attributeEditorPlugin = new AttributeEditorPlugin();
|
||||
AddInspectorPlugin(_attributeEditorPlugin);
|
||||
_sharedVariableSetInspectorPlugin = new SharedVariableSetInspectorPlugin();
|
||||
_sharedVariableSetInspectorPlugin.SetUndoRedo(GetUndoRedo());
|
||||
AddInspectorPlugin(_sharedVariableSetInspectorPlugin);
|
||||
|
||||
_statescriptGraphEditorDock = new StatescriptGraphEditorDock();
|
||||
_statescriptGraphEditorDock.SetUndoRedo(GetUndoRedo());
|
||||
AddDock(_statescriptGraphEditorDock);
|
||||
|
||||
AddToolMenuItem("Repair assets tags", new Callable(this, MethodName.CallAssetRepairTool));
|
||||
|
||||
_fileSystem = EditorInterface.Singleton.GetResourceFilesystem();
|
||||
_resourcesReimportedCallable = new Callable(this, nameof(OnResourcesReimported));
|
||||
|
||||
_fileSystem.Connect(EditorFileSystem.SignalName.ResourcesReimported, _resourcesReimportedCallable);
|
||||
|
||||
Validation.Enabled = true;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
Debug.Assert(_editorDock is not null, $"{nameof(_editorDock)} should have been initialized on _Ready().");
|
||||
Debug.Assert(_dockedScene is not null, $"{nameof(_dockedScene)} should have been initialized on _Ready().");
|
||||
Debug.Assert(
|
||||
_tagsEditorDock is not null,
|
||||
$"{nameof(_tagsEditorDock)} should have been initialized on _Ready().");
|
||||
Debug.Assert(
|
||||
_statescriptGraphEditorDock is not null,
|
||||
$"{nameof(_statescriptGraphEditorDock)} should have been initialized on _Ready().");
|
||||
|
||||
RemoveDock(_editorDock);
|
||||
_editorDock.QueueFree();
|
||||
_dockedScene.Free();
|
||||
if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _resourcesReimportedCallable)
|
||||
== true)
|
||||
{
|
||||
_fileSystem.Disconnect(EditorFileSystem.SignalName.ResourcesReimported, _resourcesReimportedCallable);
|
||||
}
|
||||
|
||||
RemoveDock(_tagsEditorDock);
|
||||
_tagsEditorDock.Free();
|
||||
|
||||
RemoveInspectorPlugin(_tagContainerInspectorPlugin);
|
||||
RemoveInspectorPlugin(_tagInspectorPlugin);
|
||||
RemoveInspectorPlugin(_attributeSetInspectorPlugin);
|
||||
RemoveInspectorPlugin(_cueHandlerInspectorPlugin);
|
||||
RemoveInspectorPlugin(_attributeEditorPlugin);
|
||||
RemoveInspectorPlugin(_sharedVariableSetInspectorPlugin);
|
||||
|
||||
RemoveDock(_statescriptGraphEditorDock);
|
||||
_statescriptGraphEditorDock.Free();
|
||||
|
||||
RemoveToolMenuItem("Repair assets tags");
|
||||
}
|
||||
|
||||
public override bool _Handles(GodotObject @object)
|
||||
{
|
||||
return @object is StatescriptGraph;
|
||||
}
|
||||
|
||||
public override void _Edit(GodotObject? @object)
|
||||
{
|
||||
if (@object is StatescriptGraph graph && _statescriptGraphEditorDock is not null)
|
||||
{
|
||||
_statescriptGraphEditorDock.OpenGraph(graph);
|
||||
}
|
||||
}
|
||||
|
||||
public override void _MakeVisible(bool visible)
|
||||
{
|
||||
if (_statescriptGraphEditorDock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible)
|
||||
{
|
||||
_statescriptGraphEditorDock.Open();
|
||||
}
|
||||
|
||||
_statescriptGraphEditorDock.Visible = visible;
|
||||
}
|
||||
|
||||
public override void _EnablePlugin()
|
||||
{
|
||||
base._EnablePlugin();
|
||||
|
||||
EnsureForgeDataExists();
|
||||
|
||||
var config = ProjectSettings.LoadResourcePack(AutoloadPath);
|
||||
|
||||
if (config)
|
||||
@@ -101,9 +156,119 @@ public partial class ForgePluginLoader : EditorPlugin
|
||||
}
|
||||
}
|
||||
|
||||
public override void _SaveExternalData()
|
||||
{
|
||||
_statescriptGraphEditorDock?.SaveAllOpenGraphs();
|
||||
}
|
||||
|
||||
public override string _GetPluginName()
|
||||
{
|
||||
return "Forge";
|
||||
}
|
||||
|
||||
public override void _GetWindowLayout(ConfigFile configuration)
|
||||
{
|
||||
if (_statescriptGraphEditorDock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var paths = _statescriptGraphEditorDock.GetOpenResourcePaths();
|
||||
|
||||
if (paths.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.SetValue("Forge", "open_tabs", string.Join(";", paths));
|
||||
configuration.SetValue("Forge", "active_tab", _statescriptGraphEditorDock.GetActiveTabIndex());
|
||||
|
||||
var varStates = _statescriptGraphEditorDock.GetVariablesPanelStates();
|
||||
configuration.SetValue("Forge", "variables_states", string.Join(";", varStates));
|
||||
}
|
||||
|
||||
public override void _SetWindowLayout(ConfigFile configuration)
|
||||
{
|
||||
if (_statescriptGraphEditorDock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Variant tabsValue = configuration.GetValue("Forge", "open_tabs", string.Empty);
|
||||
Variant active = configuration.GetValue("Forge", "active_tab", -1);
|
||||
|
||||
var tabsString = tabsValue.AsString();
|
||||
if (string.IsNullOrEmpty(tabsString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var paths = tabsString.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
var activeIndex = active.AsInt32();
|
||||
|
||||
bool[]? variablesStates = null;
|
||||
Variant varStatesValue = configuration.GetValue("Forge", "variables_states", string.Empty);
|
||||
var varString = varStatesValue.AsString();
|
||||
|
||||
if (!string.IsNullOrEmpty(varString))
|
||||
{
|
||||
var parts = varString.Split(';');
|
||||
variablesStates = new bool[parts.Length];
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
variablesStates[i] = bool.TryParse(parts[i], out var v) && v;
|
||||
}
|
||||
}
|
||||
|
||||
_statescriptGraphEditorDock.RestoreFromPaths(paths, activeIndex, variablesStates);
|
||||
}
|
||||
|
||||
private static void EnsureForgeDataExists()
|
||||
{
|
||||
if (ResourceLoader.Exists(ForgeData.ForgeDataResourcePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var forgeData = new ForgeData();
|
||||
Error error = ResourceSaver.Save(forgeData, ForgeData.ForgeDataResourcePath);
|
||||
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
GD.PrintErr($"Failed to create ForgeData resource: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
EditorInterface.Singleton.GetResourceFilesystem().Scan();
|
||||
GD.Print("Created default ForgeData resource at ", ForgeData.ForgeDataResourcePath);
|
||||
}
|
||||
|
||||
private static void CallAssetRepairTool()
|
||||
{
|
||||
AssetRepairTool.RepairAllAssetsTags();
|
||||
}
|
||||
|
||||
private void OnResourcesReimported(string[] resources)
|
||||
{
|
||||
foreach (var path in resources)
|
||||
{
|
||||
if (!ResourceLoader.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileType = EditorInterface.Singleton.GetResourceFilesystem().GetFileType(path);
|
||||
if (fileType != "StatescriptGraph" && fileType != "Resource")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Resource resource = ResourceLoader.Load(path);
|
||||
if (resource is StatescriptGraph graph)
|
||||
{
|
||||
_statescriptGraphEditorDock?.OpenGraph(graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Forge for Godot is an Unreal GAS-like gameplay framework for the Godot Engine.
|
||||
|
||||
It integrates the [Forge Gameplay System](https://github.com/gamesmiths-guild/forge) into Godot, providing a robust, data-driven foundation for gameplay features such as attributes, effects, gameplay tags, abilities, events, and cues, fully aligned with Godot’s node, resource, and editor workflows.
|
||||
It integrates the [Forge Gameplay System](https://github.com/gamesmiths-guild/forge) into Godot, providing a robust, data-driven foundation for gameplay features such as attributes, effects, gameplay tags, abilities, events, cues, and visual ability scripting through Statescript, fully aligned with Godot’s node, resource, and editor workflows.
|
||||
|
||||
This plugin enables you to:
|
||||
|
||||
@@ -12,6 +12,7 @@ This plugin enables you to:
|
||||
- Create hierarchical gameplay tags using the built-in Tags Editor.
|
||||
- Trigger visual and audio feedback with the Cues system.
|
||||
- Create player skills, attacks, or behaviors, with support for custom logic, costs, cooldowns, and triggers.
|
||||
- Build ability behaviors visually with the Statescript graph editor, or implement custom behaviors in C#.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -21,7 +22,8 @@ This plugin enables you to:
|
||||
- **Abilities System**: Feature-complete ability system, supporting grant/removal, custom behaviors, triggers, cooldowns, and costs.
|
||||
- **Events System**: Gameplay event bus supporting event-driven logic, subscriptions, and triggers.
|
||||
- **Cues System**: Visual/audio feedback layer; decouples presentation from game logic.
|
||||
- **Editor Extensions**: Custom inspector elements and tag editor with Godot integration.
|
||||
- **Statescript**: Visual state-based scripting system for implementing ability behaviors with a built-in graph editor.
|
||||
- **Editor Extensions**: Custom inspector elements, tag editor, and Statescript graph editor with Godot integration.
|
||||
- **Custom Nodes**: Includes nodes like `ForgeEntity`, `ForgeAttributeSet`, `EffectArea2D`, and more.
|
||||
|
||||
## Installation
|
||||
@@ -51,6 +53,7 @@ This plugin enables you to:
|
||||
## Documentation
|
||||
|
||||
Full documentation, examples, and advanced usage are available in the [Forge for Godot GitHub repository](https://github.com/gamesmiths-guild/forge-godot).
|
||||
For Statescript documentation, see the [Statescript guide](https://github.com/gamesmiths-guild/forge-godot/blob/main/docs/statescript/README.md).
|
||||
For technical details about core systems, see the [Forge Gameplay System documentation](https://github.com/gamesmiths-guild/forge/blob/main/docs/README.md).
|
||||
|
||||
## License
|
||||
|
||||
@@ -8,7 +8,7 @@ public partial class ForgeBootstrap : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
_ = new ForgeManagers(pluginData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Gamesmiths.Forge.Godot.Core;
|
||||
[Tool]
|
||||
public partial class ForgeData : Resource
|
||||
{
|
||||
public const string ForgeDataResourcePath = "res://forge/forge_data.tres";
|
||||
|
||||
[Export]
|
||||
public Array<string> RegisteredTags { get; set; } = [];
|
||||
}
|
||||
|
||||
352
addons/forge/core/StatescriptGraphBuilder.cs
Normal file
352
addons/forge/core/StatescriptGraphBuilder.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Gamesmiths.Forge.Statescript.Nodes;
|
||||
using Godot;
|
||||
using ForgeNode = Gamesmiths.Forge.Statescript.Node;
|
||||
using GodotVariant = Godot.Variant;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Builds a runtime <see cref="Graph"/> from a serialized <see cref="StatescriptGraph"/> resource.
|
||||
/// Resolves concrete node types from the Forge DLL and other assemblies using reflection and recreates all connections.
|
||||
/// </summary>
|
||||
public static class StatescriptGraphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a runtime <see cref="Graph"/> from the given <see cref="StatescriptGraph"/> resource.
|
||||
/// </summary>
|
||||
/// <param name="graphResource">The serialized graph resource.</param>
|
||||
/// <returns>A fully constructed runtime graph ready for execution.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a node type cannot be resolved or instantiated.
|
||||
/// </exception>
|
||||
public static Graph Build(StatescriptGraph graphResource)
|
||||
{
|
||||
var graph = new Graph();
|
||||
|
||||
var nodeMap = new Dictionary<string, ForgeNode>();
|
||||
|
||||
foreach (StatescriptNode nodeResource in graphResource.Nodes)
|
||||
{
|
||||
switch (nodeResource.NodeType)
|
||||
{
|
||||
case StatescriptNodeType.Entry:
|
||||
nodeMap[nodeResource.NodeId] = graph.EntryNode;
|
||||
break;
|
||||
|
||||
case StatescriptNodeType.Exit:
|
||||
var exitNode = new ExitNode();
|
||||
graph.AddNode(exitNode);
|
||||
nodeMap[nodeResource.NodeId] = exitNode;
|
||||
break;
|
||||
|
||||
default:
|
||||
ForgeNode runtimeNode = InstantiateNode(nodeResource);
|
||||
graph.AddNode(runtimeNode);
|
||||
nodeMap[nodeResource.NodeId] = runtimeNode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (StatescriptConnection connectionResource in graphResource.Connections)
|
||||
{
|
||||
if (!nodeMap.TryGetValue(connectionResource.FromNode, out ForgeNode? fromNode))
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Connection references unknown source node '{connectionResource.FromNode}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!nodeMap.TryGetValue(connectionResource.ToNode, out ForgeNode? toNode))
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Connection references unknown target node '{connectionResource.ToNode}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPortIndex = connectionResource.OutputPort;
|
||||
var inputPortIndex = connectionResource.InputPort;
|
||||
|
||||
if (outputPortIndex < 0 || outputPortIndex >= fromNode.OutputPorts.Length)
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Output port index {outputPortIndex} out of range on node " +
|
||||
$"'{connectionResource.FromNode}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputPortIndex < 0 || inputPortIndex >= toNode.InputPorts.Length)
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Input port index {inputPortIndex} out of range on node " +
|
||||
$"'{connectionResource.ToNode}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var connection = new Connection(
|
||||
fromNode.OutputPorts[outputPortIndex],
|
||||
toNode.InputPorts[inputPortIndex]);
|
||||
|
||||
graph.AddConnection(connection);
|
||||
}
|
||||
|
||||
RegisterGraphVariables(graph, graphResource);
|
||||
BindNodeProperties(graph, graphResource, nodeMap);
|
||||
ValidateActivationDataProviders(graphResource);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
private static void RegisterGraphVariables(Graph graph, StatescriptGraph graphResource)
|
||||
{
|
||||
foreach (StatescriptGraphVariable variable in graphResource.Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(variable.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Type clrType = StatescriptVariableTypeConverter.ToSystemType(variable.VariableType);
|
||||
|
||||
if (variable.IsArray)
|
||||
{
|
||||
var initialValues = new Variant128[variable.InitialArrayValues.Count];
|
||||
for (var i = 0; i < variable.InitialArrayValues.Count; i++)
|
||||
{
|
||||
initialValues[i] = StatescriptVariableTypeConverter.GodotVariantToForge(
|
||||
variable.InitialArrayValues[i],
|
||||
variable.VariableType);
|
||||
}
|
||||
|
||||
graph.VariableDefinitions.ArrayVariableDefinitions.Add(
|
||||
new ArrayVariableDefinition(
|
||||
new StringKey(variable.VariableName),
|
||||
initialValues,
|
||||
clrType));
|
||||
}
|
||||
else
|
||||
{
|
||||
Variant128 initialValue = StatescriptVariableTypeConverter.GodotVariantToForge(
|
||||
variable.InitialValue,
|
||||
variable.VariableType);
|
||||
|
||||
graph.VariableDefinitions.VariableDefinitions.Add(
|
||||
new VariableDefinition(
|
||||
new StringKey(variable.VariableName),
|
||||
initialValue,
|
||||
clrType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BindNodeProperties(
|
||||
Graph graph,
|
||||
StatescriptGraph graphResource,
|
||||
Dictionary<string, ForgeNode> nodeMap)
|
||||
{
|
||||
foreach (StatescriptNode nodeResource in graphResource.Nodes)
|
||||
{
|
||||
if (!nodeMap.TryGetValue(nodeResource.NodeId, out ForgeNode? runtimeNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (StatescriptNodeProperty binding in nodeResource.PropertyBindings)
|
||||
{
|
||||
if (binding.Resolver is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var index = (byte)binding.PropertyIndex;
|
||||
|
||||
if (binding.Direction == StatescriptPropertyDirection.Input)
|
||||
{
|
||||
if (index >= runtimeNode.InputProperties.Length)
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Input property index {index} out of range on node " +
|
||||
$"'{nodeResource.NodeId}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
binding.Resolver.BindInput(graph, runtimeNode, nodeResource.NodeId, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index >= runtimeNode.OutputVariables.Length)
|
||||
{
|
||||
GD.PushWarning(
|
||||
$"Statescript: Output variable index {index} out of range on node " +
|
||||
$"'{nodeResource.NodeId}'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
binding.Resolver.BindOutput(runtimeNode, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateActivationDataProviders(StatescriptGraph graphResource)
|
||||
{
|
||||
string? firstProvider = null;
|
||||
|
||||
foreach (StatescriptNode node in graphResource.Nodes)
|
||||
{
|
||||
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
|
||||
{
|
||||
if (binding.Resolver is ActivationDataResolverResource { ProviderClassName.Length: > 0 } resolver)
|
||||
{
|
||||
if (firstProvider is null)
|
||||
{
|
||||
firstProvider = resolver.ProviderClassName;
|
||||
}
|
||||
else if (resolver.ProviderClassName != firstProvider)
|
||||
{
|
||||
GD.PushError(
|
||||
"Statescript: Graph uses multiple activation data providers " +
|
||||
$"('{firstProvider}' and '{resolver.ProviderClassName}'). " +
|
||||
"A graph supports only one activation data provider at a time. " +
|
||||
"Combine the data into a single provider.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ForgeNode InstantiateNode(StatescriptNode nodeResource)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeResource.RuntimeTypeName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Node '{nodeResource.NodeId}' of type {nodeResource.NodeType} has no RuntimeTypeName set.");
|
||||
}
|
||||
|
||||
Type? nodeType = ResolveType(nodeResource.RuntimeTypeName);
|
||||
|
||||
if (nodeType is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Could not resolve runtime type '{nodeResource.RuntimeTypeName}' for node " +
|
||||
$"'{nodeResource.NodeId}'.");
|
||||
}
|
||||
|
||||
ConstructorInfo[] constructors = nodeType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
if (constructors.Length == 0)
|
||||
{
|
||||
return (ForgeNode)Activator.CreateInstance(nodeType)!;
|
||||
}
|
||||
|
||||
ConstructorInfo constructor = constructors.OrderByDescending(x => x.GetParameters().Length).First();
|
||||
ParameterInfo[] parameters = constructor.GetParameters();
|
||||
|
||||
var args = new object[parameters.Length];
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
ParameterInfo param = parameters[i];
|
||||
var paramName = param.Name ?? string.Empty;
|
||||
|
||||
if (nodeResource.CustomData.TryGetValue(paramName, out GodotVariant value))
|
||||
{
|
||||
args[i] = ConvertParameter(value, param.ParameterType);
|
||||
}
|
||||
else
|
||||
{
|
||||
args[i] = GetDefaultValue(param.ParameterType);
|
||||
}
|
||||
}
|
||||
|
||||
return (ForgeNode)constructor.Invoke(args);
|
||||
}
|
||||
|
||||
private static Type? ResolveType(string typeName)
|
||||
{
|
||||
var type = Type.GetType(typeName);
|
||||
if (type is not null)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
type = assembly.GetType(typeName);
|
||||
if (type is not null)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object ConvertParameter(GodotVariant value, Type targetType)
|
||||
{
|
||||
if (targetType == typeof(StringKey))
|
||||
{
|
||||
return new StringKey(value.AsString());
|
||||
}
|
||||
|
||||
if (targetType == typeof(string))
|
||||
{
|
||||
return value.AsString();
|
||||
}
|
||||
|
||||
if (targetType == typeof(int))
|
||||
{
|
||||
return value.AsInt32();
|
||||
}
|
||||
|
||||
if (targetType == typeof(float))
|
||||
{
|
||||
return value.AsSingle();
|
||||
}
|
||||
|
||||
if (targetType == typeof(double))
|
||||
{
|
||||
return value.AsDouble();
|
||||
}
|
||||
|
||||
if (targetType == typeof(bool))
|
||||
{
|
||||
return value.AsBool();
|
||||
}
|
||||
|
||||
if (targetType == typeof(long))
|
||||
{
|
||||
return value.AsInt64();
|
||||
}
|
||||
|
||||
return Convert.ChangeType(value.AsString(), targetType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static object GetDefaultValue(Type type)
|
||||
{
|
||||
if (type == typeof(StringKey))
|
||||
{
|
||||
return new StringKey("_default_");
|
||||
}
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (type.IsValueType)
|
||||
{
|
||||
return Activator.CreateInstance(type)!;
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/StatescriptGraphBuilder.cs.uid
Normal file
1
addons/forge/core/StatescriptGraphBuilder.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://btkf3jeisyh8j
|
||||
86
addons/forge/core/VariablesExtensions.cs
Normal file
86
addons/forge/core/VariablesExtensions.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using GodotPlane = Godot.Plane;
|
||||
using GodotQuaternion = Godot.Quaternion;
|
||||
using GodotVector2 = Godot.Vector2;
|
||||
using GodotVector3 = Godot.Vector3;
|
||||
using GodotVector4 = Godot.Vector4;
|
||||
using SysPlane = System.Numerics.Plane;
|
||||
using SysQuaternion = System.Numerics.Quaternion;
|
||||
using SysVector2 = System.Numerics.Vector2;
|
||||
using SysVector3 = System.Numerics.Vector3;
|
||||
using SysVector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Variables"/> that provide seamless support for Godot types. These methods
|
||||
/// automatically convert Godot math types (e.g., <see cref="GodotVector3"/>) to their System.Numerics equivalents
|
||||
/// before storing them in the variable bag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use these overloads in data binder delegates (e.g., when implementing
|
||||
/// <see cref="Resources.IActivationDataProvider.CreateBehavior"/>) to avoid manual Godot-to-System.Numerics
|
||||
/// conversions.
|
||||
/// </remarks>
|
||||
public static class VariablesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a variable from a <see cref="GodotVector2"/> value, converting it to <see cref="SysVector2"/>.
|
||||
/// </summary>
|
||||
/// <param name="variables">The variables bag.</param>
|
||||
/// <param name="name">The name of the variable to set.</param>
|
||||
/// <param name="value">The Godot Vector2 value to store.</param>
|
||||
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector2 value)
|
||||
{
|
||||
variables.SetVariant(name, new Variant128(new SysVector2(value.X, value.Y)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a variable from a <see cref="GodotVector3"/> value, converting it to <see cref="SysVector3"/>.
|
||||
/// </summary>
|
||||
/// <param name="variables">The variables bag.</param>
|
||||
/// <param name="name">The name of the variable to set.</param>
|
||||
/// <param name="value">The Godot Vector3 value to store.</param>
|
||||
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector3 value)
|
||||
{
|
||||
variables.SetVariant(name, new Variant128(new SysVector3(value.X, value.Y, value.Z)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a variable from a <see cref="GodotVector4"/> value, converting it to <see cref="SysVector4"/>.
|
||||
/// </summary>
|
||||
/// <param name="variables">The variables bag.</param>
|
||||
/// <param name="name">The name of the variable to set.</param>
|
||||
/// <param name="value">The Godot Vector4 value to store.</param>
|
||||
public static void SetGodotVar(this Variables variables, StringKey name, GodotVector4 value)
|
||||
{
|
||||
variables.SetVariant(name, new Variant128(new SysVector4(value.X, value.Y, value.Z, value.W)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a variable from a <see cref="GodotPlane"/> value, converting it to <see cref="SysPlane"/>.
|
||||
/// </summary>
|
||||
/// <param name="variables">The variables bag.</param>
|
||||
/// <param name="name">The name of the variable to set.</param>
|
||||
/// <param name="value">The Godot Plane value to store.</param>
|
||||
public static void SetGodotVar(this Variables variables, StringKey name, GodotPlane value)
|
||||
{
|
||||
variables.SetVariant(
|
||||
name,
|
||||
new Variant128(new SysPlane(value.Normal.X, value.Normal.Y, value.Normal.Z, value.D)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a variable from a <see cref="GodotQuaternion"/> value, converting it to <see cref="SysQuaternion"/>.
|
||||
/// </summary>
|
||||
/// <param name="variables">The variables bag.</param>
|
||||
/// <param name="name">The name of the variable to set.</param>
|
||||
/// <param name="value">The Godot Quaternion value to store.</param>
|
||||
public static void SetGodotVar(this Variables variables, StringKey name, GodotQuaternion value)
|
||||
{
|
||||
variables.SetVariant(name, new Variant128(new SysQuaternion(value.X, value.Y, value.Z, value.W)));
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/VariablesExtensions.cs.uid
Normal file
1
addons/forge/core/VariablesExtensions.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://tkifxnyfxgrp
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_resource type="Resource" load_steps=2 format=3 uid="uid://8j4xg16o3qnl"]
|
||||
[gd_resource type="Resource" format=3 uid="uid://8j4xg16o3qnl"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bq4vlbfx00hea" path="res://addons/forge/core/ForgeData.cs" id="1_x0pne"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_x0pne")
|
||||
RegisteredTags = Array[String](["effect.fire", "effect.wet", "cue.floating.text", "cue.vfx.fire", "cue.vfx.wet", "cue.vfx.regen", "cooldown.enemy.attack", "set_by_caller.damage", "event.damage", "cooldown", "cooldown.skill.projectile", "cooldown.skill.shield", "cooldown.skill.dash", "movement.block", "immunity.damage", "effect.mana_shield", "cue.vfx.shield", "event.damage.taken", "event.damage.dealt", "event", "set_by_caller", "trait.flammable", "trait.healable", "trait.damageable", "trait.wettable", "cue.vfx.reflect", "cue.vfx", "cooldown.skill", "cooldown.skill.reflect"])
|
||||
RegisteredTags = Array[String](["effect.fire", "effect.wet", "cue.floating.text", "cue.vfx.fire", "cue.vfx.wet", "cue.vfx.regen", "cooldown.enemy.attack", "set_by_caller.damage", "event.damage", "cooldown", "cooldown.skill.projectile", "cooldown.skill.shield", "cooldown.skill.dash", "movement.block", "immunity.damage", "effect.mana_shield", "cue.vfx.shield", "event.damage.taken", "event.damage.dealt", "event", "set_by_caller", "trait.flammable", "trait.healable", "trait.damageable", "trait.wettable", "cue.vfx.reflect", "cue.vfx", "cooldown.skill", "cooldown.skill.reflect", "test"])
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class AssetRepairTool : EditorPlugin
|
||||
{
|
||||
public static void RepairAllAssetsTags()
|
||||
{
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
|
||||
List<string> scenes = GetScenePaths("res://");
|
||||
|
||||
@@ -19,11 +19,9 @@ internal static class EditorUtils
|
||||
{
|
||||
var options = new List<string>();
|
||||
|
||||
// Get all types in the current assembly
|
||||
Type[] allTypes = Assembly.GetExecutingAssembly().GetTypes();
|
||||
|
||||
// Find all types that subclass AttributeSet
|
||||
foreach (Type attributeSetType in allTypes.Where(x => x.IsSubclassOf(typeof(AttributeSet))))
|
||||
foreach (Type attributeSetType in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(x => x.IsSubclassOf(typeof(AttributeSet))))
|
||||
{
|
||||
options.Add(attributeSetType.Name);
|
||||
}
|
||||
@@ -43,10 +41,9 @@ internal static class EditorUtils
|
||||
return [];
|
||||
}
|
||||
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
Type? type = Array.Find(
|
||||
asm.GetTypes(),
|
||||
x => x.IsSubclassOf(typeof(AttributeSet)) && x.Name == attributeSet);
|
||||
Type? type = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(x => x.IsSubclassOf(typeof(AttributeSet)) && x.Name == attributeSet);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Godot;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeEditorProperty : EditorProperty
|
||||
public partial class AttributeEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private const int ButtonSize = 26;
|
||||
private const int PopupSize = 300;
|
||||
@@ -15,17 +15,15 @@ public partial class AttributeEditorProperty : EditorProperty
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiDropdown", "EditorIcons");
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("GuiDropdown", "EditorIcons");
|
||||
|
||||
var hbox = new HBoxContainer();
|
||||
var hBox = new HBoxContainer();
|
||||
_label = new Label { Text = "None", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
var button = new Button { Icon = dropdownIcon, CustomMinimumSize = new Vector2(ButtonSize, 0) };
|
||||
|
||||
hbox.AddChild(_label);
|
||||
hbox.AddChild(button);
|
||||
AddChild(hbox);
|
||||
hBox.AddChild(_label);
|
||||
hBox.AddChild(button);
|
||||
AddChild(hBox);
|
||||
|
||||
var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) };
|
||||
var tree = new Tree
|
||||
@@ -78,6 +76,20 @@ public partial class AttributeEditorProperty : EditorProperty
|
||||
_label.Text = string.IsNullOrEmpty(value) ? "None" : value;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
for (var i = GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = GetChild(i);
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
|
||||
private static void BuildAttributeTree(Tree tree)
|
||||
{
|
||||
TreeItem root = tree.CreateItem();
|
||||
|
||||
@@ -12,7 +12,7 @@ using Godot.Collections;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetClassEditorProperty : EditorProperty
|
||||
public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private OptionButton _optionButton = null!;
|
||||
|
||||
@@ -32,26 +32,40 @@ public partial class AttributeSetClassEditorProperty : EditorProperty
|
||||
var className = _optionButton.GetItemText((int)x);
|
||||
EmitChanged(GetEditedProperty(), className);
|
||||
|
||||
GodotObject obj = GetEditedObject();
|
||||
if (obj is not null)
|
||||
GodotObject @object = GetEditedObject();
|
||||
if (@object is not null)
|
||||
{
|
||||
var dict = new Dictionary<string, AttributeValues>();
|
||||
var dictionary = new Dictionary<string, AttributeValues>();
|
||||
|
||||
var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet));
|
||||
Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className);
|
||||
if (targetType is not null)
|
||||
{
|
||||
System.Collections.Generic.IEnumerable<PropertyInfo> attrProps = targetType
|
||||
System.Collections.Generic.IEnumerable<PropertyInfo> attributeProperties = targetType
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
foreach (PropertyInfo? pi in attrProps)
|
||||
foreach (var propertyName in attributeProperties.Select(x => x.Name))
|
||||
{
|
||||
dict[pi.Name] = new AttributeValues(0, 0, int.MaxValue);
|
||||
if (@object is not ForgeAttributeSet forgeAttributeSet)
|
||||
{
|
||||
dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeSet? attributeSet = forgeAttributeSet.GetAttributeSet();
|
||||
if (attributeSet is null)
|
||||
{
|
||||
dictionary[propertyName] = new AttributeValues(0, 0, int.MaxValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityAttribute key = attributeSet.AttributesMap[className + "." + propertyName];
|
||||
dictionary[propertyName] = new AttributeValues(key.CurrentValue, key.Min, key.Max);
|
||||
}
|
||||
}
|
||||
|
||||
EmitChanged("InitialAttributeValues", dict);
|
||||
EmitChanged("InitialAttributeValues", dictionary);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -70,5 +84,19 @@ public partial class AttributeSetClassEditorProperty : EditorProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
for (var i = GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = GetChild(i);
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -13,7 +13,7 @@ using Godot.Collections;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetValuesEditorProperty : EditorProperty
|
||||
public partial class AttributeSetValuesEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
@@ -94,6 +94,24 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
VBoxContainer? attributesRoot = GetNodeOrNull<VBoxContainer>("AttributesRoot");
|
||||
if (attributesRoot is not null)
|
||||
{
|
||||
for (var i = attributesRoot.GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = attributesRoot.GetChild(i);
|
||||
attributesRoot.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
|
||||
private static PanelContainer AttributeHeader(string text)
|
||||
{
|
||||
var headerPanel = new PanelContainer
|
||||
@@ -124,17 +142,17 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty
|
||||
|
||||
private static HBoxContainer AttributeFieldRow(string label, SpinBox spinBox)
|
||||
{
|
||||
var hbox = new HBoxContainer();
|
||||
var hBox = new HBoxContainer();
|
||||
|
||||
hbox.AddChild(new Label
|
||||
hBox.AddChild(new Label
|
||||
{
|
||||
Text = label,
|
||||
CustomMinimumSize = new Vector2(80, 0),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
hbox.AddChild(spinBox);
|
||||
return hbox;
|
||||
hBox.AddChild(spinBox);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
private static SpinBox CreateSpinBox(int min, int max, int value)
|
||||
@@ -143,8 +161,9 @@ public partial class AttributeSetValuesEditorProperty : EditorProperty
|
||||
{
|
||||
MinValue = min,
|
||||
MaxValue = max,
|
||||
Value = value,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SelectAllOnFocus = true,
|
||||
Value = value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Godot;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeValues : Resource
|
||||
public partial class AttributeValues : RefCounted
|
||||
{
|
||||
[Export]
|
||||
public int Default { get; set; }
|
||||
|
||||
@@ -13,27 +13,22 @@ public partial class CueHandlerInspectorPlugin : EditorInspectorPlugin
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
// Find out if its an implementation of CueHandler without having to add [Tool] attribute to them.
|
||||
try
|
||||
if (@object?.GetScript().As<CSharpScript>() is CSharpScript script)
|
||||
{
|
||||
if (@object.GetScript().As<CSharpScript>() is not { }) return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
StringName className = script.GetGlobalName();
|
||||
|
||||
Type baseType = typeof(ForgeCueHandler);
|
||||
System.Reflection.Assembly assembly = baseType.Assembly;
|
||||
|
||||
Type? implementationType =
|
||||
Array.Find(assembly.GetTypes(), x =>
|
||||
x.Name == className &&
|
||||
baseType.IsAssignableFrom(x));
|
||||
|
||||
return implementationType is not null;
|
||||
}
|
||||
|
||||
var script = @object.GetScript().As<CSharpScript>();
|
||||
StringName className = script.GetGlobalName();
|
||||
|
||||
Type baseType = typeof(ForgeCueHandler);
|
||||
System.Reflection.Assembly assembly = baseType.Assembly;
|
||||
|
||||
Type? implementationType =
|
||||
Array.Find(assembly.GetTypes(), x =>
|
||||
x.Name == className &&
|
||||
baseType.IsAssignableFrom(x));
|
||||
|
||||
return implementationType is not null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
|
||||
@@ -47,7 +47,7 @@ public partial class CueKeyEditorProperty : EditorProperty
|
||||
|
||||
AddChild(popup);
|
||||
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
TreeItem root = tree.CreateItem();
|
||||
BuildTreeRecursively(tree, root, tagsManager.RootNode);
|
||||
|
||||
279
addons/forge/editor/statescript/CustomNodeEditor.cs
Normal file
279
addons/forge/editor/statescript/CustomNodeEditor.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for custom node property editors. Implementations override the default input-property / output-variable
|
||||
/// sections rendered by <see cref="StatescriptGraphNode"/> for specific node types. Analogous to Godot's
|
||||
/// <c>EditorInspectorPlugin</c> pattern.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If a <see cref="CustomNodeEditor"/> is registered for a node's <c>RuntimeTypeName</c>, its
|
||||
/// <see cref="BuildPropertySections"/> method is called instead of the default property rendering. The base class
|
||||
/// provides helper methods that mirror the default behavior so that custom editors can reuse them selectively.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because this class extends <see cref="RefCounted"/>, signal handlers defined on subclasses can be connected
|
||||
/// directly to Godot signals (e.g. <c>dropdown.ItemSelected += OnItemSelected</c>) without needing wrapper nodes
|
||||
/// or workarounds for serialization.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Tool]
|
||||
internal abstract partial class CustomNodeEditor : RefCounted
|
||||
{
|
||||
private StatescriptGraphNode? _graphNode;
|
||||
private StatescriptGraph? _graph;
|
||||
private StatescriptNode? _nodeResource;
|
||||
private Dictionary<PropertySlotKey, NodeEditorProperty>? _activeResolverEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the runtime type name this editor handles (e.g.,
|
||||
/// <c>"Gamesmiths.Forge.Statescript.Nodes.Action.SetVariableNode"</c>).
|
||||
/// </summary>
|
||||
public abstract string HandledRuntimeTypeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds the custom input-property and output-variable sections for the node.
|
||||
/// </summary>
|
||||
/// <param name="typeInfo">Discovered metadata about the node type.</param>
|
||||
public abstract void BuildPropertySections(StatescriptNodeDiscovery.NodeTypeInfo typeInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input property section color.
|
||||
/// </summary>
|
||||
protected static Color InputPropertyColor { get; } = new(0x61afefff);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output variable section color.
|
||||
/// </summary>
|
||||
protected static Color OutputVariableColor { get; } = new(0xe5c07bff);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active resolver editors dictionary.
|
||||
/// </summary>
|
||||
protected Dictionary<PropertySlotKey, NodeEditorProperty> ActiveResolverEditors => _activeResolverEditors!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the owning graph resource.
|
||||
/// </summary>
|
||||
protected StatescriptGraph Graph => _graph!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node resource.
|
||||
/// </summary>
|
||||
protected StatescriptNode NodeResource => _nodeResource!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo/redo manager, if available.
|
||||
/// </summary>
|
||||
protected EditorUndoRedoManager? UndoRedo => _graphNode?.GetUndoRedo();
|
||||
|
||||
/// <summary>
|
||||
/// Stores references needed by helper methods. Called once after the instance is created.
|
||||
/// </summary>
|
||||
/// <param name="graphNode">The graph node this editor is bound to.</param>
|
||||
/// <param name="graph">The graph resource this node belongs to.</param>
|
||||
/// <param name="nodeResource">The node resource being edited.</param>
|
||||
/// <param name="activeResolverEditors">A dictionary of active resolver editors.</param>
|
||||
internal void Bind(
|
||||
StatescriptGraphNode graphNode,
|
||||
StatescriptGraph graph,
|
||||
StatescriptNode nodeResource,
|
||||
Dictionary<PropertySlotKey, NodeEditorProperty> activeResolverEditors)
|
||||
{
|
||||
_graphNode = graphNode;
|
||||
_graph = graph;
|
||||
_nodeResource = nodeResource;
|
||||
_activeResolverEditors = activeResolverEditors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all references stored by <see cref="Bind"/>. Called before the owning graph node is freed or serialized
|
||||
/// to prevent accessing disposed objects.
|
||||
/// </summary>
|
||||
internal virtual void Unbind()
|
||||
{
|
||||
_graphNode = null;
|
||||
_graph = null;
|
||||
_nodeResource = null;
|
||||
_activeResolverEditors = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all children from a container control.
|
||||
/// </summary>
|
||||
/// <param name="container">The container control to clear.</param>
|
||||
protected static void ClearContainer(Control container)
|
||||
{
|
||||
foreach (Node child in container.GetChildren())
|
||||
{
|
||||
container.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a foldable section divider to the graph node.
|
||||
/// </summary>
|
||||
/// <param name="sectionTitle">Title displayed on the divider.</param>
|
||||
/// <param name="color">Color of the divider.</param>
|
||||
/// <param name="foldKey">Key used to persist the fold state.</param>
|
||||
/// <param name="folded">Initial fold state.</param>
|
||||
protected FoldableContainer AddPropertySectionDivider(
|
||||
string sectionTitle,
|
||||
Color color,
|
||||
string foldKey,
|
||||
bool folded)
|
||||
{
|
||||
return _graphNode!.AddPropertySectionDividerInternal(sectionTitle, color, foldKey, folded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a standard input-property row (resolver dropdown + editor UI).
|
||||
/// </summary>
|
||||
/// <param name="propInfo">Metadata about the input property.</param>
|
||||
/// <param name="index">Index of the input property.</param>
|
||||
/// <param name="container">Container to add the input property row to.</param>
|
||||
protected void AddInputPropertyRow(
|
||||
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
|
||||
int index,
|
||||
Control container)
|
||||
{
|
||||
_graphNode!.AddInputPropertyRowInternal(propInfo, index, container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a standard output-variable row (variable dropdown).
|
||||
/// </summary>
|
||||
/// <param name="varInfo">Metadata about the output variable.</param>
|
||||
/// <param name="index">Index of the output variable.</param>
|
||||
/// <param name="container">Container to add the output variable row to.</param>
|
||||
protected void AddOutputVariableRow(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
FoldableContainer container)
|
||||
{
|
||||
_graphNode!.AddOutputVariableRowInternal(varInfo, index, container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted fold state for a given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key used to persist the fold state.</param>
|
||||
protected bool GetFoldState(string key)
|
||||
{
|
||||
return _graphNode!.GetFoldStateInternal(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an existing property binding by direction and index.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction of the property (input or output).</param>
|
||||
/// <param name="propertyIndex">The index of the property.</param>
|
||||
protected StatescriptNodeProperty? FindBinding(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
return _graphNode!.FindBindingInternal(direction, propertyIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a property binding exists for the given direction and index, creating one if needed.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction of the property (input or output).</param>
|
||||
/// <param name="propertyIndex">The index of the property.</param>
|
||||
protected StatescriptNodeProperty EnsureBinding(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
return _graphNode!.EnsureBindingInternal(direction, propertyIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property binding by direction and index.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction of the property (input or output).</param>
|
||||
/// <param name="propertyIndex">The index of the property.</param>
|
||||
protected void RemoveBinding(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
_graphNode!.RemoveBindingInternal(direction, propertyIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a resolver editor inside the given container.
|
||||
/// </summary>
|
||||
/// <param name="factory">A factory function to create the resolver editor.</param>
|
||||
/// <param name="existingBinding">The existing binding, if any.</param>
|
||||
/// <param name="expectedType">The expected type for the resolver editor.</param>
|
||||
/// <param name="container">The container to add the resolver editor to.</param>
|
||||
/// <param name="direction">The direction of the property (input or output).</param>
|
||||
/// <param name="propertyIndex">The index of the property.</param>
|
||||
/// <param name="isArray">Whether the input expects an array of values.</param>
|
||||
protected void ShowResolverEditorUI(
|
||||
Func<NodeEditorProperty> factory,
|
||||
StatescriptNodeProperty? existingBinding,
|
||||
Type expectedType,
|
||||
VBoxContainer container,
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex,
|
||||
bool isArray = false)
|
||||
{
|
||||
_graphNode!.ShowResolverEditorUIInternal(
|
||||
factory,
|
||||
existingBinding,
|
||||
expectedType,
|
||||
container,
|
||||
direction,
|
||||
propertyIndex,
|
||||
isArray);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the owning graph node to recalculate its size.
|
||||
/// </summary>
|
||||
protected void ResetSize()
|
||||
{
|
||||
_graphNode!.ResetSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="StatescriptGraphNode.PropertyBindingChanged"/> event.
|
||||
/// </summary>
|
||||
protected void RaisePropertyBindingChanged()
|
||||
{
|
||||
_graphNode!.RaisePropertyBindingChangedInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an undo/redo action for changing a resolver binding, then rebuilds the node.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction of the property.</param>
|
||||
/// <param name="propertyIndex">The index of the property.</param>
|
||||
/// <param name="oldResolver">The previous resolver resource.</param>
|
||||
/// <param name="newResolver">The new resolver resource.</param>
|
||||
/// <param name="actionName">The name for the undo/redo action.</param>
|
||||
protected void RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex,
|
||||
StatescriptResolverResource? oldResolver,
|
||||
StatescriptResolverResource? newResolver,
|
||||
string actionName = "Change Node Property")
|
||||
{
|
||||
_graphNode!.RecordResolverBindingChangeInternal(
|
||||
direction,
|
||||
propertyIndex,
|
||||
oldResolver,
|
||||
newResolver,
|
||||
actionName);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/statescript/CustomNodeEditor.cs.uid
Normal file
1
addons/forge/editor/statescript/CustomNodeEditor.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://f47pprjqcskr
|
||||
52
addons/forge/editor/statescript/CustomNodeEditorRegistry.cs
Normal file
52
addons/forge/editor/statescript/CustomNodeEditorRegistry.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of <see cref="CustomNodeEditor"/> implementations. Custom node editors are discovered automatically via
|
||||
/// reflection. Any concrete subclass of <see cref="CustomNodeEditor"/> in the executing assembly is registered and
|
||||
/// overrides the default property rendering for its handled node type.
|
||||
/// </summary>
|
||||
internal static class CustomNodeEditorRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, Func<CustomNodeEditor>> _factories = [];
|
||||
|
||||
static CustomNodeEditorRegistry()
|
||||
{
|
||||
Type[] allTypes = Assembly.GetExecutingAssembly().GetTypes();
|
||||
|
||||
foreach (Type type in allTypes.Where(
|
||||
x => x.IsSubclassOf(typeof(CustomNodeEditor)) && !x.IsAbstract))
|
||||
{
|
||||
Type captured = type;
|
||||
using var temp = (CustomNodeEditor)Activator.CreateInstance(captured)!;
|
||||
_factories[temp.HandledRuntimeTypeName] = () => (CustomNodeEditor)Activator.CreateInstance(captured)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a new custom node editor for the given runtime type name.
|
||||
/// </summary>
|
||||
/// <param name="runtimeTypeName">The runtime type name of the node.</param>
|
||||
/// <param name="editor">The newly created editor, or <see langword="null"/> if none is registered.</param>
|
||||
/// <returns><see langword="true"/> if a custom editor was created.</returns>
|
||||
public static bool TryCreate(string runtimeTypeName, [NotNullWhen(true)] out CustomNodeEditor? editor)
|
||||
{
|
||||
if (_factories.TryGetValue(runtimeTypeName, out Func<CustomNodeEditor>? factory))
|
||||
{
|
||||
editor = factory();
|
||||
return true;
|
||||
}
|
||||
|
||||
editor = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dk4rjrm6ky3rd
|
||||
78
addons/forge/editor/statescript/NodeEditorProperty.cs
Normal file
78
addons/forge/editor/statescript/NodeEditorProperty.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all Statescript property resolver editor controls. Extends <see cref="PanelContainer"/> so it can be
|
||||
/// added directly to the graph node UI and participates in the Godot scene tree (lifecycle, disposal, etc.).
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal abstract partial class NodeEditorProperty : PanelContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display name shown in the resolver type dropdown (e.g., "Variable", "Constant", "Attribute").
|
||||
/// </summary>
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolver type identifier string used for matching against serialized resources.
|
||||
/// </summary>
|
||||
public abstract string ResolverTypeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this resolver is compatible with the given expected type.
|
||||
/// </summary>
|
||||
/// <param name="expectedType">The type expected by the node's input property.</param>
|
||||
/// <returns><see langword="true"/> if this resolver can provide a value of the expected type.</returns>
|
||||
public abstract bool IsCompatibleWith(Type expectedType);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the resolver editor UI. Called once after the control is created.
|
||||
/// </summary>
|
||||
/// <param name="graph">The current graph resource (for accessing variables, etc.).</param>
|
||||
/// <param name="property">The existing property binding to restore state from, or null for a new binding.</param>
|
||||
/// <param name="expectedType">The type expected by the node's input property.</param>
|
||||
/// <param name="onChanged">Callback invoked when the resolver configuration changes.</param>
|
||||
/// <param name="isArray">Whether the input expects an array of values.</param>
|
||||
public abstract void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the current resolver configuration to the given property binding resource.
|
||||
/// </summary>
|
||||
/// <param name="property">The property binding to write to.</param>
|
||||
public abstract void SaveTo(StatescriptNodeProperty property);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the editor's layout size has changed (e.g. nested resolver swap, foldable toggle) so that the owning
|
||||
/// <see cref="GraphNode"/> can call <see cref="Control.ResetSize"/>.
|
||||
/// </summary>
|
||||
public event Action? LayoutSizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Clears all delegate fields to prevent serialization issues during hot-reload. Called before the editor is
|
||||
/// serialized or freed.
|
||||
/// </summary>
|
||||
public virtual void ClearCallbacks()
|
||||
{
|
||||
LayoutSizeChanged = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies listeners that the editor layout has changed size.
|
||||
/// </summary>
|
||||
protected void RaiseLayoutSizeChanged()
|
||||
{
|
||||
LayoutSizeChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://djb18x1m1rukn
|
||||
@@ -0,0 +1,798 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Custom <see cref="EditorProperty"/> that renders the <see cref="ForgeSharedVariableSet.Variables"/> array using the
|
||||
/// same polished value-editor controls as the graph variable panel.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class SharedVariableSetEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private static readonly Color _variableColor = new(0xe5c07bff);
|
||||
|
||||
private readonly HashSet<string> _expandedArrays = [];
|
||||
|
||||
private EditorUndoRedoManager? _undoRedo;
|
||||
|
||||
private VBoxContainer? _root;
|
||||
private VBoxContainer? _variableList;
|
||||
private Button? _addButton;
|
||||
|
||||
private AcceptDialog? _creationDialog;
|
||||
private LineEdit? _newNameEdit;
|
||||
private OptionButton? _newTypeDropdown;
|
||||
private CheckBox? _newArrayToggle;
|
||||
|
||||
private Texture2D? _addIcon;
|
||||
private Texture2D? _removeIcon;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
||||
/// </summary>
|
||||
/// <param name="undoRedo">The undo/redo manager from the editor plugin.</param>
|
||||
public void SetUndoRedo(EditorUndoRedoManager? undoRedo)
|
||||
{
|
||||
_undoRedo = undoRedo;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
_removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
var backgroundPanel = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var panelStyle = new StyleBoxFlat
|
||||
{
|
||||
BgColor = EditorInterface.Singleton.GetEditorTheme().GetColor("base_color", "Editor"),
|
||||
ContentMarginLeft = 6,
|
||||
ContentMarginRight = 6,
|
||||
ContentMarginTop = 4,
|
||||
ContentMarginBottom = 4,
|
||||
CornerRadiusTopLeft = 3,
|
||||
CornerRadiusTopRight = 3,
|
||||
CornerRadiusBottomLeft = 3,
|
||||
CornerRadiusBottomRight = 3,
|
||||
};
|
||||
|
||||
backgroundPanel.AddThemeStyleboxOverride("panel", panelStyle);
|
||||
AddChild(backgroundPanel);
|
||||
SetBottomEditor(backgroundPanel);
|
||||
|
||||
_root = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
backgroundPanel.AddChild(_root);
|
||||
|
||||
var headerHBox = new HBoxContainer();
|
||||
_root.AddChild(headerHBox);
|
||||
|
||||
_addButton = new Button
|
||||
{
|
||||
Text = "Add Variable",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_addButton.Pressed += OnAddPressed;
|
||||
headerHBox.AddChild(_addButton);
|
||||
|
||||
_root.AddChild(new HSeparator());
|
||||
|
||||
_variableList = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_root.AddChild(_variableList);
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
if (_addButton is not null)
|
||||
{
|
||||
_addButton.Pressed -= OnAddPressed;
|
||||
}
|
||||
|
||||
ClearVariableList();
|
||||
|
||||
_creationDialog?.Free();
|
||||
_creationDialog = null;
|
||||
_newNameEdit = null;
|
||||
_newTypeDropdown = null;
|
||||
_newArrayToggle = null;
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
if (_addButton is not null)
|
||||
{
|
||||
_addButton.Pressed += OnAddPressed;
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private Array<ForgeSharedVariableDefinition> GetDefinitions()
|
||||
{
|
||||
GodotObject obj = GetEditedObject();
|
||||
string propertyName = GetEditedProperty();
|
||||
Variant value = obj.Get(propertyName);
|
||||
|
||||
return value.AsGodotArray<ForgeSharedVariableDefinition>() ?? [];
|
||||
}
|
||||
|
||||
private void NotifyChanged()
|
||||
{
|
||||
if (GetEditedObject() is Resource resource)
|
||||
{
|
||||
resource.EmitChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildList()
|
||||
{
|
||||
if (_variableList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer the actual rebuild so that any in-progress signal emission (e.g. a button Pressed handler that
|
||||
// triggered an add/remove) finishes before we free the emitting nodes.
|
||||
CallDeferred(MethodName.RebuildListDeferred);
|
||||
}
|
||||
|
||||
private void RebuildListDeferred()
|
||||
{
|
||||
if (_variableList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearVariableList();
|
||||
|
||||
Array<ForgeSharedVariableDefinition> definitions = GetDefinitions();
|
||||
|
||||
for (var i = 0; i < definitions.Count; i++)
|
||||
{
|
||||
AddVariableRow(definitions, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearVariableList()
|
||||
{
|
||||
if (_variableList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in _variableList.GetChildren())
|
||||
{
|
||||
_variableList.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVariableRow(Array<ForgeSharedVariableDefinition> definitions, int index)
|
||||
{
|
||||
if (_variableList is null || index < 0 || index >= definitions.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ForgeSharedVariableDefinition def = definitions[index];
|
||||
|
||||
var rowContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_variableList.AddChild(rowContainer);
|
||||
|
||||
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
rowContainer.AddChild(headerRow);
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = def.VariableName,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
nameLabel.AddThemeColorOverride("font_color", _variableColor);
|
||||
nameLabel.AddThemeFontOverride(
|
||||
"font",
|
||||
EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts"));
|
||||
headerRow.AddChild(nameLabel);
|
||||
|
||||
var typeLabel = new Label
|
||||
{
|
||||
Text = $"({StatescriptVariableTypeConverter.GetDisplayName(def.VariableType)}"
|
||||
+ (def.IsArray ? "[])" : ")"),
|
||||
};
|
||||
|
||||
typeLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f));
|
||||
headerRow.AddChild(typeLabel);
|
||||
|
||||
var capturedIndex = index;
|
||||
|
||||
var deleteButton = new Button
|
||||
{
|
||||
Icon = _removeIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Remove Variable",
|
||||
CustomMinimumSize = new Vector2(28, 28),
|
||||
};
|
||||
|
||||
deleteButton.Pressed += () => OnDeletePressed(capturedIndex);
|
||||
headerRow.AddChild(deleteButton);
|
||||
|
||||
if (!def.IsArray)
|
||||
{
|
||||
Control valueEditor = CreateValueEditor(def);
|
||||
rowContainer.AddChild(valueEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
VBoxContainer arrayEditor = CreateArrayValueEditor(def);
|
||||
rowContainer.AddChild(arrayEditor);
|
||||
}
|
||||
|
||||
rowContainer.AddChild(new HSeparator());
|
||||
}
|
||||
|
||||
private Control CreateValueEditor(ForgeSharedVariableDefinition def)
|
||||
{
|
||||
if (def.VariableType == StatescriptVariableType.Bool)
|
||||
{
|
||||
var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
hBox.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
def.InitialValue.AsBool(),
|
||||
x => SetVariableValue(def, Variant.From(x))));
|
||||
|
||||
return hBox;
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsIntegerType(def.VariableType)
|
||||
|| StatescriptEditorControls.IsFloatType(def.VariableType))
|
||||
{
|
||||
var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
def.VariableType,
|
||||
def.InitialValue.AsDouble(),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.IsIntegerType(def.VariableType)
|
||||
? Variant.From((long)x)
|
||||
: Variant.From(x);
|
||||
SetVariableValue(def, newValue);
|
||||
});
|
||||
|
||||
hBox.AddChild(spin);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(def.VariableType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateVectorEditor(
|
||||
def.VariableType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(
|
||||
def.InitialValue,
|
||||
def.VariableType,
|
||||
x),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.BuildVectorVariant(
|
||||
def.VariableType,
|
||||
x);
|
||||
SetVariableValue(def, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
var fallback = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
fallback.AddChild(new Label { Text = def.VariableType.ToString() });
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private VBoxContainer CreateArrayValueEditor(ForgeSharedVariableDefinition def)
|
||||
{
|
||||
var vBox = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var headerRow = new HBoxContainer();
|
||||
vBox.AddChild(headerRow);
|
||||
|
||||
var isExpanded = _expandedArrays.Contains(def.VariableName);
|
||||
|
||||
var elementsContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Visible = isExpanded,
|
||||
};
|
||||
|
||||
var toggleButton = new Button
|
||||
{
|
||||
Text = $"Array (size {def.InitialArrayValues.Count})",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = isExpanded,
|
||||
};
|
||||
|
||||
toggleButton.Toggled += x =>
|
||||
{
|
||||
elementsContainer.Visible = x;
|
||||
|
||||
var wasExpanded = !x;
|
||||
|
||||
if (x)
|
||||
{
|
||||
_expandedArrays.Add(def.VariableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_expandedArrays.Remove(def.VariableName);
|
||||
}
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Toggle Array Expand");
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoSetArrayExpanded,
|
||||
def.VariableName,
|
||||
x);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.DoSetArrayExpanded,
|
||||
def.VariableName,
|
||||
wasExpanded);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
headerRow.AddChild(toggleButton);
|
||||
|
||||
var addElementButton = new Button
|
||||
{
|
||||
Icon = _addIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Add Element",
|
||||
CustomMinimumSize = new Vector2(24, 24),
|
||||
};
|
||||
|
||||
addElementButton.Pressed += () =>
|
||||
{
|
||||
Variant defaultValue =
|
||||
StatescriptVariableTypeConverter.CreateDefaultGodotVariant(def.VariableType);
|
||||
AddArrayElement(def, defaultValue);
|
||||
};
|
||||
|
||||
headerRow.AddChild(addElementButton);
|
||||
|
||||
vBox.AddChild(elementsContainer);
|
||||
|
||||
for (var i = 0; i < def.InitialArrayValues.Count; i++)
|
||||
{
|
||||
var capturedIndex = i;
|
||||
|
||||
if (def.VariableType == StatescriptVariableType.Bool)
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
def.InitialArrayValues[i].AsBool(),
|
||||
x => SetArrayElementValue(def, capturedIndex, Variant.From(x))));
|
||||
|
||||
AddArrayElementRemoveButton(elementRow, def, capturedIndex);
|
||||
}
|
||||
else if (StatescriptEditorControls.IsVectorType(def.VariableType))
|
||||
{
|
||||
var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementVBox);
|
||||
|
||||
var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementVBox.AddChild(labelRow);
|
||||
labelRow.AddChild(new Label
|
||||
{
|
||||
Text = $"[{i}]",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
AddArrayElementRemoveButton(labelRow, def, capturedIndex);
|
||||
|
||||
VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor(
|
||||
def.VariableType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(
|
||||
def.InitialArrayValues[capturedIndex],
|
||||
def.VariableType,
|
||||
x),
|
||||
x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.BuildVectorVariant(
|
||||
def.VariableType,
|
||||
x);
|
||||
SetArrayElementValue(def, capturedIndex, newValue);
|
||||
});
|
||||
|
||||
elementVBox.AddChild(vectorEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
EditorSpinSlider elementSpin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
def.VariableType,
|
||||
def.InitialArrayValues[i].AsDouble(),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.IsIntegerType(def.VariableType)
|
||||
? Variant.From((long)x)
|
||||
: Variant.From(x);
|
||||
SetArrayElementValue(def, capturedIndex, newValue);
|
||||
});
|
||||
|
||||
elementRow.AddChild(elementSpin);
|
||||
AddArrayElementRemoveButton(elementRow, def, capturedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private void AddArrayElementRemoveButton(
|
||||
HBoxContainer row,
|
||||
ForgeSharedVariableDefinition def,
|
||||
int elementIndex)
|
||||
{
|
||||
var removeElementButton = new Button
|
||||
{
|
||||
Icon = _removeIcon,
|
||||
Flat = true,
|
||||
CustomMinimumSize = new Vector2(24, 24),
|
||||
};
|
||||
|
||||
removeElementButton.Pressed += () => RemoveArrayElement(def, elementIndex);
|
||||
|
||||
row.AddChild(removeElementButton);
|
||||
}
|
||||
|
||||
private void SetVariableValue(ForgeSharedVariableDefinition def, Variant newValue)
|
||||
{
|
||||
Variant oldValue = def.InitialValue;
|
||||
|
||||
def.InitialValue = newValue;
|
||||
NotifyChanged();
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction($"Change Shared Variable '{def.VariableName}'");
|
||||
_undoRedo.AddDoMethod(this, MethodName.ApplyVariableValue, def, newValue);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.ApplyVariableValue, def, oldValue);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetArrayElementValue(ForgeSharedVariableDefinition def, int index, Variant newValue)
|
||||
{
|
||||
Variant oldValue = def.InitialArrayValues[index];
|
||||
|
||||
def.InitialArrayValues[index] = newValue;
|
||||
NotifyChanged();
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction($"Change Shared Variable '{def.VariableName}' Element [{index}]");
|
||||
_undoRedo.AddDoMethod(this, MethodName.ApplyArrayElementValue, def, index, newValue);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.ApplyArrayElementValue, def, index, oldValue);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddArrayElement(ForgeSharedVariableDefinition def, Variant value)
|
||||
{
|
||||
var wasExpanded = _expandedArrays.Contains(def.VariableName);
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction($"Add Element to '{def.VariableName}'");
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoAddArrayElement, def, value);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoAddArrayElement, def, wasExpanded);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoAddArrayElement(def, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveArrayElement(ForgeSharedVariableDefinition def, int index)
|
||||
{
|
||||
if (index < 0 || index >= def.InitialArrayValues.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Variant oldValue = def.InitialArrayValues[index];
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction($"Remove Element [{index}] from '{def.VariableName}'");
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoRemoveArrayElement, def, index);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoRemoveArrayElement, def, index, oldValue);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoRemoveArrayElement(def, index);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddPressed()
|
||||
{
|
||||
ShowCreationDialog();
|
||||
}
|
||||
|
||||
private void ShowCreationDialog()
|
||||
{
|
||||
_creationDialog?.QueueFree();
|
||||
|
||||
_creationDialog = new AcceptDialog
|
||||
{
|
||||
Title = "Add Shared Variable",
|
||||
Size = new Vector2I(300, 160),
|
||||
Exclusive = true,
|
||||
};
|
||||
|
||||
var vBox = new VBoxContainer();
|
||||
_creationDialog.AddChild(vBox);
|
||||
|
||||
var nameRow = new HBoxContainer();
|
||||
vBox.AddChild(nameRow);
|
||||
nameRow.AddChild(new Label { Text = "Name:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newNameEdit = new LineEdit
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
PlaceholderText = "variable name",
|
||||
};
|
||||
|
||||
nameRow.AddChild(_newNameEdit);
|
||||
|
||||
var typeRow = new HBoxContainer();
|
||||
vBox.AddChild(typeRow);
|
||||
typeRow.AddChild(new Label { Text = "Type:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newTypeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
foreach (StatescriptVariableType variableType in StatescriptVariableTypeConverter.GetAllTypes())
|
||||
{
|
||||
_newTypeDropdown.AddItem(
|
||||
StatescriptVariableTypeConverter.GetDisplayName(variableType),
|
||||
(int)variableType);
|
||||
}
|
||||
|
||||
typeRow.AddChild(_newTypeDropdown);
|
||||
|
||||
var arrayRow = new HBoxContainer();
|
||||
vBox.AddChild(arrayRow);
|
||||
arrayRow.AddChild(new Label { Text = "Array:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newArrayToggle = new CheckBox();
|
||||
arrayRow.AddChild(_newArrayToggle);
|
||||
|
||||
_creationDialog.Confirmed += OnCreationConfirmed;
|
||||
_creationDialog.Canceled += OnCreationCanceled;
|
||||
|
||||
EditorInterface.Singleton.PopupDialogCentered(_creationDialog);
|
||||
}
|
||||
|
||||
private void OnCreationConfirmed()
|
||||
{
|
||||
if (_newNameEdit is null || _newTypeDropdown is null || _newArrayToggle is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var name = _newNameEdit.Text.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var variableType = (StatescriptVariableType)_newTypeDropdown.GetItemId(_newTypeDropdown.Selected);
|
||||
|
||||
var newDef = new ForgeSharedVariableDefinition
|
||||
{
|
||||
VariableName = name,
|
||||
VariableType = variableType,
|
||||
IsArray = _newArrayToggle.ButtonPressed,
|
||||
InitialValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(variableType),
|
||||
};
|
||||
|
||||
Array<ForgeSharedVariableDefinition> definitions = GetDefinitions();
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Add Shared Variable");
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoAddVariable, definitions, newDef);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, definitions, newDef);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoAddVariable(definitions, newDef);
|
||||
}
|
||||
|
||||
CleanupCreationDialog();
|
||||
}
|
||||
|
||||
private void OnCreationCanceled()
|
||||
{
|
||||
CleanupCreationDialog();
|
||||
}
|
||||
|
||||
private void CleanupCreationDialog()
|
||||
{
|
||||
_creationDialog?.QueueFree();
|
||||
_creationDialog = null;
|
||||
_newNameEdit = null;
|
||||
_newTypeDropdown = null;
|
||||
_newArrayToggle = null;
|
||||
}
|
||||
|
||||
private void OnDeletePressed(int index)
|
||||
{
|
||||
Array<ForgeSharedVariableDefinition> definitions = GetDefinitions();
|
||||
|
||||
if (index < 0 || index >= definitions.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ForgeSharedVariableDefinition variable = definitions[index];
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Remove Shared Variable");
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, definitions, variable, index);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, definitions, variable, index);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoRemoveVariable(definitions, index);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVariableValue(ForgeSharedVariableDefinition def, Variant value)
|
||||
{
|
||||
def.InitialValue = value;
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void ApplyArrayElementValue(ForgeSharedVariableDefinition def, int index, Variant value)
|
||||
{
|
||||
def.InitialArrayValues[index] = value;
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void DoAddVariable(Array<ForgeSharedVariableDefinition> definitions, ForgeSharedVariableDefinition def)
|
||||
{
|
||||
definitions.Add(def);
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoAddVariable(Array<ForgeSharedVariableDefinition> definitions, ForgeSharedVariableDefinition def)
|
||||
{
|
||||
definitions.Remove(def);
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void DoRemoveVariable(
|
||||
Array<ForgeSharedVariableDefinition> definitions,
|
||||
int index)
|
||||
{
|
||||
definitions.RemoveAt(index);
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoRemoveVariable(
|
||||
Array<ForgeSharedVariableDefinition> definitions,
|
||||
ForgeSharedVariableDefinition sharedVariableDefinition,
|
||||
int index)
|
||||
{
|
||||
if (index >= definitions.Count)
|
||||
{
|
||||
definitions.Add(sharedVariableDefinition);
|
||||
}
|
||||
else
|
||||
{
|
||||
definitions.Insert(index, sharedVariableDefinition);
|
||||
}
|
||||
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void DoAddArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, Variant value)
|
||||
{
|
||||
sharedVariableDefinition.InitialArrayValues.Add(value);
|
||||
_expandedArrays.Add(sharedVariableDefinition.VariableName);
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoAddArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, bool wasExpanded)
|
||||
{
|
||||
if (sharedVariableDefinition.InitialArrayValues.Count > 0)
|
||||
{
|
||||
sharedVariableDefinition.InitialArrayValues.RemoveAt(sharedVariableDefinition.InitialArrayValues.Count - 1);
|
||||
}
|
||||
|
||||
if (!wasExpanded)
|
||||
{
|
||||
_expandedArrays.Remove(sharedVariableDefinition.VariableName);
|
||||
}
|
||||
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void DoRemoveArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, int index)
|
||||
{
|
||||
sharedVariableDefinition.InitialArrayValues.RemoveAt(index);
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoRemoveArrayElement(
|
||||
ForgeSharedVariableDefinition sharedVariableDefinition,
|
||||
int index,
|
||||
Variant value)
|
||||
{
|
||||
if (index >= sharedVariableDefinition.InitialArrayValues.Count)
|
||||
{
|
||||
sharedVariableDefinition.InitialArrayValues.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sharedVariableDefinition.InitialArrayValues.Insert(index, value);
|
||||
}
|
||||
|
||||
NotifyChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void DoSetArrayExpanded(string variableName, bool expanded)
|
||||
{
|
||||
if (expanded)
|
||||
{
|
||||
_expandedArrays.Add(variableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_expandedArrays.Remove(variableName);
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://co05oybb4l5fp
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Inspector plugin that replaces the default <see cref="ForgeSharedVariableSet.Variables"/> array editor with a
|
||||
/// polished UI matching the graph variable panel style.
|
||||
/// </summary>
|
||||
public partial class SharedVariableSetInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
private EditorUndoRedoManager? _undoRedo;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
||||
/// </summary>
|
||||
/// <param name="undoRedo">The undo/redo manager from the editor plugin.</param>
|
||||
public void SetUndoRedo(EditorUndoRedoManager undoRedo)
|
||||
{
|
||||
_undoRedo = undoRedo;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is ForgeSharedVariableSet;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name != "Variables")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var editorProperty = new SharedVariableSetEditorProperty();
|
||||
editorProperty.SetUndoRedo(_undoRedo);
|
||||
AddPropertyEditor(name, editorProperty);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi7wqecgc87xl
|
||||
425
addons/forge/editor/statescript/StatescriptAddNodeDialog.cs
Normal file
425
addons/forge/editor/statescript/StatescriptAddNodeDialog.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Statescript.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// A popup dialog for adding Statescript nodes to a graph. Features a search bar, categorized tree view, description
|
||||
/// panel, and Create/Cancel buttons.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class StatescriptAddNodeDialog : ConfirmationDialog, ISerializationListener
|
||||
{
|
||||
private const int DialogWidth = 244;
|
||||
private const int DialogHeight = 400;
|
||||
|
||||
private static readonly string _exitNodeDescription = new ExitNode().Description;
|
||||
|
||||
private LineEdit? _searchBar;
|
||||
private MenuButton? _expandCollapseButton;
|
||||
private PopupMenu? _expandCollapsePopup;
|
||||
private Tree? _tree;
|
||||
private Label? _descriptionHeader;
|
||||
private RichTextLabel? _descriptionLabel;
|
||||
|
||||
private bool _isFiltering;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user confirms node creation. The first argument is the selected
|
||||
/// <see cref="StatescriptNodeDiscovery.NodeTypeInfo"/> (null for Exit node), the second is the
|
||||
/// <see cref="StatescriptNodeType"/>, and the third is the graph-local position to place the node.
|
||||
/// </summary>
|
||||
public event Action<StatescriptNodeDiscovery.NodeTypeInfo?, StatescriptNodeType, Vector2>? NodeCreationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the graph-local position where the new node should be placed.
|
||||
/// </summary>
|
||||
public Vector2 SpawnPosition { get; set; }
|
||||
|
||||
public StatescriptAddNodeDialog()
|
||||
{
|
||||
Title = "Add Statescript Node";
|
||||
Exclusive = true;
|
||||
Unresizable = false;
|
||||
MinSize = new Vector2I(DialogWidth, DialogHeight);
|
||||
Size = new Vector2I(DialogWidth, DialogHeight);
|
||||
OkButtonText = "Create";
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
Transient = true;
|
||||
TransientToFocused = true;
|
||||
|
||||
BuildUI();
|
||||
PopulateTree();
|
||||
|
||||
GetOkButton().Disabled = true;
|
||||
|
||||
Confirmed += OnConfirmed;
|
||||
Canceled += OnCanceled;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
base._ExitTree();
|
||||
DisconnectSignals();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
DisconnectSignals();
|
||||
NodeCreationRequested = null;
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
ConnectSignals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the dialog at the specified screen position, resets search and selection state.
|
||||
/// </summary>
|
||||
/// <param name="spawnPosition">The graph-local position where the node should be created.</param>
|
||||
/// <param name="screenPosition">The screen position to show the dialog at.</param>
|
||||
public void ShowAtPosition(Vector2 spawnPosition, Vector2I screenPosition)
|
||||
{
|
||||
SpawnPosition = spawnPosition;
|
||||
|
||||
if (_isFiltering)
|
||||
{
|
||||
_searchBar?.Clear();
|
||||
PopulateTree();
|
||||
}
|
||||
else
|
||||
{
|
||||
_searchBar?.Clear();
|
||||
}
|
||||
|
||||
_tree?.DeselectAll();
|
||||
GetOkButton().Disabled = true;
|
||||
UpdateDescription(null);
|
||||
|
||||
Position = screenPosition;
|
||||
Size = new Vector2I(DialogWidth, DialogHeight);
|
||||
Popup();
|
||||
|
||||
_searchBar?.GrabFocus();
|
||||
}
|
||||
|
||||
private static void SetAllCollapsed(TreeItem root, bool collapsed)
|
||||
{
|
||||
TreeItem? child = root.GetFirstChild();
|
||||
while (child is not null)
|
||||
{
|
||||
child.Collapsed = collapsed;
|
||||
SetAllCollapsed(child, collapsed);
|
||||
child = child.GetNext();
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
var vBox = new VBoxContainer
|
||||
{
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(vBox);
|
||||
|
||||
var searchHBox = new HBoxContainer();
|
||||
vBox.AddChild(searchHBox);
|
||||
|
||||
_searchBar = new LineEdit
|
||||
{
|
||||
PlaceholderText = "Search...",
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
ClearButtonEnabled = true,
|
||||
RightIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Search", "EditorIcons"),
|
||||
};
|
||||
|
||||
_searchBar.TextChanged += OnSearchTextChanged;
|
||||
searchHBox.AddChild(_searchBar);
|
||||
|
||||
_expandCollapseButton = new MenuButton
|
||||
{
|
||||
Flat = true,
|
||||
Icon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Tools", "EditorIcons"),
|
||||
TooltipText = "Options",
|
||||
};
|
||||
|
||||
_expandCollapsePopup = _expandCollapseButton.GetPopup();
|
||||
_expandCollapsePopup.AddItem("Expand All", 0);
|
||||
_expandCollapsePopup.AddItem("Collapse All", 1);
|
||||
_expandCollapsePopup.IdPressed += OnExpandCollapseMenuPressed;
|
||||
searchHBox.AddChild(_expandCollapseButton);
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
HideRoot = true,
|
||||
SelectMode = Tree.SelectModeEnum.Single,
|
||||
};
|
||||
|
||||
_tree.ItemSelected += OnTreeItemSelected;
|
||||
_tree.ItemActivated += OnTreeItemActivated;
|
||||
vBox.AddChild(_tree);
|
||||
|
||||
_descriptionHeader = new Label
|
||||
{
|
||||
Text = "Description:",
|
||||
};
|
||||
|
||||
vBox.AddChild(_descriptionHeader);
|
||||
|
||||
_descriptionLabel = new RichTextLabel
|
||||
{
|
||||
BbcodeEnabled = true,
|
||||
ScrollActive = true,
|
||||
CustomMinimumSize = new Vector2(0, 70),
|
||||
};
|
||||
|
||||
vBox.AddChild(_descriptionLabel);
|
||||
}
|
||||
|
||||
private void PopulateTree(string filter = "")
|
||||
{
|
||||
if (_tree is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isFiltering = !string.IsNullOrWhiteSpace(filter);
|
||||
_tree.Clear();
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
IReadOnlyList<StatescriptNodeDiscovery.NodeTypeInfo> discoveredTypes =
|
||||
StatescriptNodeDiscovery.GetDiscoveredNodeTypes();
|
||||
|
||||
var filterLower = filter.ToLowerInvariant();
|
||||
|
||||
TreeItem? actionCategory = null;
|
||||
TreeItem? conditionCategory = null;
|
||||
TreeItem? stateCategory = null;
|
||||
|
||||
foreach (StatescriptNodeDiscovery.NodeTypeInfo typeInfo in discoveredTypes)
|
||||
{
|
||||
if (_isFiltering && !typeInfo.DisplayName.Contains(filterLower, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TreeItem categoryItem;
|
||||
|
||||
switch (typeInfo.NodeType)
|
||||
{
|
||||
case StatescriptNodeType.Action:
|
||||
actionCategory ??= CreateCategoryItem(root, "Action");
|
||||
categoryItem = actionCategory;
|
||||
break;
|
||||
case StatescriptNodeType.Condition:
|
||||
conditionCategory ??= CreateCategoryItem(root, "Condition");
|
||||
categoryItem = conditionCategory;
|
||||
break;
|
||||
case StatescriptNodeType.State:
|
||||
stateCategory ??= CreateCategoryItem(root, "State");
|
||||
categoryItem = stateCategory;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
TreeItem item = _tree.CreateItem(categoryItem);
|
||||
item.SetText(0, typeInfo.DisplayName);
|
||||
item.SetMetadata(0, typeInfo.RuntimeTypeName);
|
||||
}
|
||||
|
||||
if (!_isFiltering || "exit".Contains(filterLower, StringComparison.OrdinalIgnoreCase)
|
||||
|| "exit node".Contains(filterLower, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TreeItem exitItem = _tree.CreateItem(root);
|
||||
exitItem.SetText(0, "Exit");
|
||||
exitItem.SetMetadata(0, "__exit__");
|
||||
}
|
||||
|
||||
SetAllCollapsed(root, !_isFiltering);
|
||||
|
||||
UpdateDescription(null);
|
||||
}
|
||||
|
||||
private TreeItem CreateCategoryItem(TreeItem parent, string name)
|
||||
{
|
||||
TreeItem item = _tree!.CreateItem(parent);
|
||||
item.SetText(0, name);
|
||||
item.SetSelectable(0, false);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void OnSearchTextChanged(string newText)
|
||||
{
|
||||
PopulateTree(newText);
|
||||
GetOkButton().Disabled = true;
|
||||
}
|
||||
|
||||
private void OnExpandCollapseMenuPressed(long id)
|
||||
{
|
||||
if (_tree is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TreeItem? root = _tree.GetRoot();
|
||||
if (root is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetAllCollapsed(root, id != 0);
|
||||
}
|
||||
|
||||
private void OnTreeItemSelected()
|
||||
{
|
||||
if (_tree is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TreeItem? selected = _tree.GetSelected();
|
||||
if (selected?.IsSelectable(0) != true)
|
||||
{
|
||||
GetOkButton().Disabled = true;
|
||||
UpdateDescription(null);
|
||||
return;
|
||||
}
|
||||
|
||||
GetOkButton().Disabled = false;
|
||||
|
||||
var metadata = selected.GetMetadata(0).AsString();
|
||||
UpdateDescription(metadata);
|
||||
}
|
||||
|
||||
private void OnTreeItemActivated()
|
||||
{
|
||||
if (_tree?.GetSelected() is not null && !GetOkButton().Disabled)
|
||||
{
|
||||
OnConfirmed();
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfirmed()
|
||||
{
|
||||
if (_tree is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TreeItem? selected = _tree.GetSelected();
|
||||
if (selected?.IsSelectable(0) != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = selected.GetMetadata(0).AsString();
|
||||
|
||||
if (metadata == "__exit__")
|
||||
{
|
||||
NodeCreationRequested?.Invoke(null, StatescriptNodeType.Exit, SpawnPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
StatescriptNodeDiscovery.NodeTypeInfo? typeInfo =
|
||||
StatescriptNodeDiscovery.FindByRuntimeTypeName(metadata);
|
||||
|
||||
if (typeInfo is not null)
|
||||
{
|
||||
NodeCreationRequested?.Invoke(typeInfo, typeInfo.NodeType, SpawnPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanceled()
|
||||
{
|
||||
// Method intentionally left blank, no action needed on cancel.
|
||||
}
|
||||
|
||||
private void UpdateDescription(string? runtimeTypeName)
|
||||
{
|
||||
if (_descriptionLabel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtimeTypeName is null)
|
||||
{
|
||||
_descriptionLabel.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtimeTypeName == "__exit__")
|
||||
{
|
||||
_descriptionLabel.Text = _exitNodeDescription;
|
||||
return;
|
||||
}
|
||||
|
||||
StatescriptNodeDiscovery.NodeTypeInfo? typeInfo =
|
||||
StatescriptNodeDiscovery.FindByRuntimeTypeName(runtimeTypeName);
|
||||
|
||||
_descriptionLabel.Text = typeInfo?.Description ?? string.Empty;
|
||||
}
|
||||
|
||||
private void DisconnectSignals()
|
||||
{
|
||||
Confirmed -= OnConfirmed;
|
||||
Canceled -= OnCanceled;
|
||||
|
||||
if (_searchBar is not null)
|
||||
{
|
||||
_searchBar.TextChanged -= OnSearchTextChanged;
|
||||
}
|
||||
|
||||
if (_expandCollapsePopup is not null)
|
||||
{
|
||||
_expandCollapsePopup.IdPressed -= OnExpandCollapseMenuPressed;
|
||||
}
|
||||
|
||||
if (_tree is not null)
|
||||
{
|
||||
_tree.ItemSelected -= OnTreeItemSelected;
|
||||
_tree.ItemActivated -= OnTreeItemActivated;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConnectSignals()
|
||||
{
|
||||
Confirmed += OnConfirmed;
|
||||
Canceled += OnCanceled;
|
||||
|
||||
if (_searchBar is not null)
|
||||
{
|
||||
_searchBar.TextChanged += OnSearchTextChanged;
|
||||
}
|
||||
|
||||
if (_expandCollapsePopup is not null)
|
||||
{
|
||||
_expandCollapsePopup.IdPressed += OnExpandCollapseMenuPressed;
|
||||
}
|
||||
|
||||
if (_tree is not null)
|
||||
{
|
||||
_tree.ItemSelected += OnTreeItemSelected;
|
||||
_tree.ItemActivated += OnTreeItemActivated;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcwnu7ebs2h1c
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Signal handler helpers used by <see cref="StatescriptEditorControls"/> to avoid lambdas on Godot signals.
|
||||
/// Each handler is a <see cref="Node"/> so it can be parented to its owning control and freed automatically.
|
||||
/// </summary>
|
||||
internal static partial class StatescriptEditorControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <see cref="BaseButton.Toggled"/> for boolean editors, forwarding to an <see cref="Action{T}"/>.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class BoolSignalHandler : Node
|
||||
{
|
||||
public Action<bool>? OnChanged { get; set; }
|
||||
|
||||
public void HandleToggled(bool pressed)
|
||||
{
|
||||
OnChanged?.Invoke(pressed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="EditorSpinSlider"/> signals (<c>ValueChanged</c>, <c>Grabbed</c>, <c>Ungrabbed</c>,
|
||||
/// <c>FocusExited</c>) for numeric editors with drag-commit semantics.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class NumericSpinHandler : Node
|
||||
{
|
||||
private readonly EditorSpinSlider _spin;
|
||||
private bool _isDragging;
|
||||
|
||||
public Action<double>? OnChanged { get; set; }
|
||||
|
||||
public NumericSpinHandler()
|
||||
{
|
||||
_spin = null!;
|
||||
}
|
||||
|
||||
public NumericSpinHandler(EditorSpinSlider spin)
|
||||
{
|
||||
_spin = spin;
|
||||
}
|
||||
|
||||
public void HandleValueChanged(double value)
|
||||
{
|
||||
if (!_isDragging)
|
||||
{
|
||||
OnChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGrabbed()
|
||||
{
|
||||
_isDragging = true;
|
||||
}
|
||||
|
||||
public void HandleUngrabbed()
|
||||
{
|
||||
_isDragging = false;
|
||||
OnChanged?.Invoke(_spin.Value);
|
||||
}
|
||||
|
||||
public void HandleFocusExited()
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the shared state (values array, drag flag, callback) for a multi-component vector editor.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VectorComponentHandler : Node
|
||||
{
|
||||
private readonly double[] _values;
|
||||
|
||||
public Action<double[]>? OnChanged { get; set; }
|
||||
|
||||
public bool IsDragging { get; set; }
|
||||
|
||||
public VectorComponentHandler()
|
||||
{
|
||||
_values = [];
|
||||
}
|
||||
|
||||
public VectorComponentHandler(double[] values)
|
||||
{
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public void SetValue(int index, double value)
|
||||
{
|
||||
_values[index] = value;
|
||||
}
|
||||
|
||||
public void RaiseChanged()
|
||||
{
|
||||
OnChanged?.Invoke(_values);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="EditorSpinSlider"/> signals for a single component of a vector editor.
|
||||
/// Forwards to the shared <see cref="VectorComponentHandler"/>.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VectorSpinHandler : Node
|
||||
{
|
||||
private readonly VectorComponentHandler _parent;
|
||||
private readonly int _componentIndex;
|
||||
|
||||
public VectorSpinHandler()
|
||||
{
|
||||
_parent = null!;
|
||||
}
|
||||
|
||||
public VectorSpinHandler(VectorComponentHandler parent, int componentIndex)
|
||||
{
|
||||
_parent = parent;
|
||||
_componentIndex = componentIndex;
|
||||
}
|
||||
|
||||
public void HandleValueChanged(double value)
|
||||
{
|
||||
_parent.SetValue(_componentIndex, value);
|
||||
|
||||
if (!_parent.IsDragging)
|
||||
{
|
||||
_parent.RaiseChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGrabbed()
|
||||
{
|
||||
_parent.IsDragging = true;
|
||||
}
|
||||
|
||||
public void HandleUngrabbed()
|
||||
{
|
||||
_parent.IsDragging = false;
|
||||
_parent.RaiseChanged();
|
||||
}
|
||||
|
||||
public void HandleFocusExited()
|
||||
{
|
||||
_parent.IsDragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cssljh632gdln
|
||||
421
addons/forge/editor/statescript/StatescriptEditorControls.cs
Normal file
421
addons/forge/editor/statescript/StatescriptEditorControls.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Shared factory methods for creating value-editor controls used by both the variable panel and resolver editors.
|
||||
/// </summary>
|
||||
internal static partial class StatescriptEditorControls
|
||||
{
|
||||
private static readonly Color _axisXColor = new(0.96f, 0.37f, 0.37f);
|
||||
private static readonly Color _axisYColor = new(0.54f, 0.83f, 0.01f);
|
||||
private static readonly Color _axisZColor = new(0.33f, 0.55f, 0.96f);
|
||||
private static readonly Color _axisWColor = new(0.66f, 0.66f, 0.66f);
|
||||
|
||||
private static StyleBox? _cachedPanelStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> for integer-like variable types.
|
||||
/// </summary>
|
||||
/// <param name="type">The variable type to check.</param>
|
||||
public static bool IsIntegerType(StatescriptVariableType type)
|
||||
{
|
||||
return type is StatescriptVariableType.Int or StatescriptVariableType.UInt
|
||||
or StatescriptVariableType.Long or StatescriptVariableType.ULong
|
||||
or StatescriptVariableType.Short or StatescriptVariableType.UShort
|
||||
or StatescriptVariableType.Byte or StatescriptVariableType.SByte
|
||||
or StatescriptVariableType.Char;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> for floating-point variable types.
|
||||
/// </summary>
|
||||
/// <param name="type">The variable type to check.</param>
|
||||
public static bool IsFloatType(StatescriptVariableType type)
|
||||
{
|
||||
return type is StatescriptVariableType.Float or StatescriptVariableType.Double
|
||||
or StatescriptVariableType.Decimal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> for multi-component vector/quaternion/plane variable types.
|
||||
/// </summary>
|
||||
/// <param name="type">The variable type to check.</param>
|
||||
public static bool IsVectorType(StatescriptVariableType type)
|
||||
{
|
||||
return type is StatescriptVariableType.Vector2 or StatescriptVariableType.Vector3
|
||||
or StatescriptVariableType.Vector4 or StatescriptVariableType.Plane
|
||||
or StatescriptVariableType.Quaternion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PanelContainer"/> wrapping a <see cref="CheckBox"/> for boolean editing.
|
||||
/// </summary>
|
||||
/// <param name="value">The initial value of the boolean.</param>
|
||||
/// <param name="onChanged">An action invoked on value change.</param>
|
||||
/// <returns>A <see cref="PanelContainer"/> containing a <see cref="CheckBox"/>.</returns>
|
||||
public static PanelContainer CreateBoolEditor(bool value, Action<bool> onChanged)
|
||||
{
|
||||
var container = new PanelContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
container.AddThemeStyleboxOverride("panel", GetPanelStyle());
|
||||
|
||||
var checkButton = new CheckBox
|
||||
{
|
||||
Text = "On",
|
||||
ButtonPressed = value,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var handler = new BoolSignalHandler { OnChanged = onChanged };
|
||||
checkButton.AddChild(handler);
|
||||
checkButton.Toggled += handler.HandleToggled;
|
||||
container.AddChild(checkButton);
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="EditorSpinSlider"/> configured for the given numeric variable type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the numeric variable.</param>
|
||||
/// <param name="value">The initial value of the numeric variable.</param>
|
||||
/// <param name="onChanged">An action invoked on value change.</param>
|
||||
/// <returns>An <see cref="EditorSpinSlider"/> configured for the specified numeric variable type.</returns>
|
||||
public static EditorSpinSlider CreateNumericSpinSlider(
|
||||
StatescriptVariableType type,
|
||||
double value,
|
||||
Action<double>? onChanged = null)
|
||||
{
|
||||
NumericConfig config = GetNumericConfig(type);
|
||||
|
||||
var spin = new EditorSpinSlider
|
||||
{
|
||||
Step = config.Step,
|
||||
Rounded = config.IsInteger,
|
||||
EditingInteger = config.IsInteger,
|
||||
MinValue = config.MinValue,
|
||||
MaxValue = config.MaxValue,
|
||||
AllowGreater = config.AllowBeyondRange,
|
||||
AllowLesser = config.AllowBeyondRange,
|
||||
ControlState = config.IsInteger
|
||||
? EditorSpinSlider.ControlStateEnum.Default
|
||||
: EditorSpinSlider.ControlStateEnum.Hide,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
Value = value,
|
||||
};
|
||||
|
||||
var handler = new NumericSpinHandler(spin) { OnChanged = onChanged };
|
||||
spin.AddChild(handler);
|
||||
|
||||
if (onChanged is not null)
|
||||
{
|
||||
spin.ValueChanged += handler.HandleValueChanged;
|
||||
}
|
||||
|
||||
spin.Grabbed += handler.HandleGrabbed;
|
||||
spin.Ungrabbed += handler.HandleUngrabbed;
|
||||
spin.FocusExited += handler.HandleFocusExited;
|
||||
|
||||
return spin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a panel with a row of labelled <see cref="EditorSpinSlider"/> controls for editing a vector value.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the vector/quaternion/plane.</param>
|
||||
/// <param name="getComponent">A function to retrieve the value of a specific component.</param>
|
||||
/// <param name="onChanged">An action to invoke when any component value changes.</param>
|
||||
/// <returns>A <see cref="VBoxContainer"/> containing the vector editor controls.</returns>
|
||||
public static VBoxContainer CreateVectorEditor(
|
||||
StatescriptVariableType type,
|
||||
Func<int, double> getComponent,
|
||||
Action<double[]>? onChanged)
|
||||
{
|
||||
var componentCount = GetVectorComponentCount(type);
|
||||
var labels = GetVectorComponentLabels(type);
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
|
||||
var row = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
row.AddThemeConstantOverride("separation", 0);
|
||||
|
||||
var panelContainer = new PanelContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
|
||||
panelContainer.AddThemeStyleboxOverride("panel", GetPanelStyle());
|
||||
|
||||
vBox.AddChild(panelContainer);
|
||||
panelContainer.AddChild(row);
|
||||
|
||||
var values = new double[componentCount];
|
||||
var handler = new VectorComponentHandler(values) { OnChanged = onChanged };
|
||||
vBox.AddChild(handler);
|
||||
|
||||
for (var i = 0; i < componentCount; i++)
|
||||
{
|
||||
values[i] = getComponent(i);
|
||||
|
||||
var spin = new EditorSpinSlider
|
||||
{
|
||||
Label = labels[i],
|
||||
Step = 0.001,
|
||||
Rounded = false,
|
||||
EditingInteger = false,
|
||||
AllowGreater = true,
|
||||
AllowLesser = true,
|
||||
Flat = false,
|
||||
ControlState = EditorSpinSlider.ControlStateEnum.Hide,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
CustomMinimumSize = new Vector2(71, 0),
|
||||
Value = values[i],
|
||||
};
|
||||
|
||||
spin.AddThemeColorOverride("label_color", GetComponentColor(i));
|
||||
|
||||
var componentHandler = new VectorSpinHandler(handler, i);
|
||||
spin.AddChild(componentHandler);
|
||||
|
||||
spin.ValueChanged += componentHandler.HandleValueChanged;
|
||||
spin.Grabbed += componentHandler.HandleGrabbed;
|
||||
spin.Ungrabbed += componentHandler.HandleUngrabbed;
|
||||
spin.FocusExited += componentHandler.HandleFocusExited;
|
||||
|
||||
row.AddChild(spin);
|
||||
}
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single component from a vector/quaternion/plane variant.
|
||||
/// </summary>
|
||||
/// <param name="value">The variant containing the vector/quaternion/plane value.</param>
|
||||
/// <param name="type">The type of the vector/quaternion/plane.</param>
|
||||
/// <param name="index">The index of the component to retrieve.</param>
|
||||
/// <exception cref="NotImplementedException">Exception thrown if the provided type is not a vector/quaternion/plane
|
||||
/// type.</exception>
|
||||
public static double GetVectorComponent(Variant value, StatescriptVariableType type, int index)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
StatescriptVariableType.Vector2 => index == 0
|
||||
? value.AsVector2().X
|
||||
: value.AsVector2().Y,
|
||||
StatescriptVariableType.Vector3 => index switch
|
||||
{
|
||||
0 => value.AsVector3().X,
|
||||
1 => value.AsVector3().Y,
|
||||
_ => value.AsVector3().Z,
|
||||
},
|
||||
StatescriptVariableType.Vector4 => index switch
|
||||
{
|
||||
0 => value.AsVector4().X,
|
||||
1 => value.AsVector4().Y,
|
||||
2 => value.AsVector4().Z,
|
||||
_ => value.AsVector4().W,
|
||||
},
|
||||
StatescriptVariableType.Plane => index switch
|
||||
{
|
||||
0 => value.AsPlane().Normal.X,
|
||||
1 => value.AsPlane().Normal.Y,
|
||||
2 => value.AsPlane().Normal.Z,
|
||||
_ => value.AsPlane().D,
|
||||
},
|
||||
StatescriptVariableType.Quaternion => index switch
|
||||
{
|
||||
0 => value.AsQuaternion().X,
|
||||
1 => value.AsQuaternion().Y,
|
||||
2 => value.AsQuaternion().Z,
|
||||
_ => value.AsQuaternion().W,
|
||||
},
|
||||
StatescriptVariableType.Bool => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Byte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.SByte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Char => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Decimal => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Double => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Float => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Int => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UInt => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Long => throw new NotImplementedException(),
|
||||
StatescriptVariableType.ULong => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Short => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UShort => throw new NotImplementedException(),
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a Godot <see cref="Variant"/> from a component array for the given vector/quaternion/plane type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the vector/quaternion/plane.</param>
|
||||
/// <param name="values">The array of component values.</param>
|
||||
/// <returns>A <see cref="Variant"/> representing the vector/quaternion/plane.</returns>
|
||||
/// <exception cref="NotImplementedException">Exception thrown if the provided type is not a vector/quaternion/plane
|
||||
/// type.</exception>
|
||||
public static Variant BuildVectorVariant(StatescriptVariableType type, double[] values)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
StatescriptVariableType.Vector2 => Variant.From(
|
||||
new Vector2((float)values[0], (float)values[1])),
|
||||
StatescriptVariableType.Vector3 => Variant.From(
|
||||
new Vector3(
|
||||
(float)values[0],
|
||||
(float)values[1],
|
||||
(float)values[2])),
|
||||
StatescriptVariableType.Vector4 => Variant.From(
|
||||
new Vector4(
|
||||
(float)values[0],
|
||||
(float)values[1],
|
||||
(float)values[2],
|
||||
(float)values[3])),
|
||||
StatescriptVariableType.Plane => Variant.From(
|
||||
new Plane(
|
||||
new Vector3(
|
||||
(float)values[0],
|
||||
(float)values[1],
|
||||
(float)values[2]),
|
||||
(float)values[3])),
|
||||
StatescriptVariableType.Quaternion => Variant.From(
|
||||
new Quaternion(
|
||||
(float)values[0],
|
||||
(float)values[1],
|
||||
(float)values[2],
|
||||
(float)values[3])),
|
||||
StatescriptVariableType.Bool => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Byte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.SByte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Char => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Decimal => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Double => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Float => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Int => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UInt => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Long => throw new NotImplementedException(),
|
||||
StatescriptVariableType.ULong => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Short => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UShort => throw new NotImplementedException(),
|
||||
_ => Variant.From(0),
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetVectorComponentCount(StatescriptVariableType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
StatescriptVariableType.Vector2 => 2,
|
||||
StatescriptVariableType.Vector3 => 3,
|
||||
StatescriptVariableType.Vector4 => 4,
|
||||
StatescriptVariableType.Plane => 4,
|
||||
StatescriptVariableType.Quaternion => 4,
|
||||
StatescriptVariableType.Bool => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Byte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.SByte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Char => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Decimal => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Double => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Float => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Int => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UInt => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Long => throw new NotImplementedException(),
|
||||
StatescriptVariableType.ULong => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Short => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UShort => throw new NotImplementedException(),
|
||||
_ => 4,
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GetVectorComponentLabels(StatescriptVariableType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
StatescriptVariableType.Vector2 => ["x", "y"],
|
||||
StatescriptVariableType.Vector3 => ["x", "y", "z"],
|
||||
StatescriptVariableType.Plane => ["x", "y", "z", "d"],
|
||||
StatescriptVariableType.Vector4 => ["x", "y", "z", "w"],
|
||||
StatescriptVariableType.Quaternion => ["x", "y", "z", "w"],
|
||||
StatescriptVariableType.Bool => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Byte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.SByte => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Char => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Decimal => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Double => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Float => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Int => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UInt => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Long => throw new NotImplementedException(),
|
||||
StatescriptVariableType.ULong => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Short => throw new NotImplementedException(),
|
||||
StatescriptVariableType.UShort => throw new NotImplementedException(),
|
||||
_ => ["x", "y", "z", "w"],
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetComponentColor(int index)
|
||||
{
|
||||
return index switch
|
||||
{
|
||||
0 => _axisXColor,
|
||||
1 => _axisYColor,
|
||||
2 => _axisZColor,
|
||||
_ => _axisWColor,
|
||||
};
|
||||
}
|
||||
|
||||
private static NumericConfig GetNumericConfig(StatescriptVariableType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
StatescriptVariableType.Byte => new NumericConfig(byte.MinValue, byte.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.SByte => new NumericConfig(sbyte.MinValue, sbyte.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.Char => new NumericConfig(char.MinValue, char.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.Short => new NumericConfig(short.MinValue, short.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.UShort => new NumericConfig(ushort.MinValue, ushort.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.Int => new NumericConfig(int.MinValue, int.MaxValue, 1, true, false),
|
||||
StatescriptVariableType.UInt => new NumericConfig(uint.MinValue, uint.MaxValue, 1, true, false),
|
||||
|
||||
// Godot's interface starts acting weird if we try to use the full range of long/ulong, so we clamp to +/-
|
||||
// 9e18 which should be sufficient for most use cases.
|
||||
StatescriptVariableType.Long => new NumericConfig(-9e18, 9e18, 1, true, false),
|
||||
StatescriptVariableType.ULong => new NumericConfig(0, 9e18, 1, true, false),
|
||||
StatescriptVariableType.Float => new NumericConfig(-1e10, 1e10, 0.001, false, true),
|
||||
StatescriptVariableType.Double => new NumericConfig(-1e10, 1e10, 0.001, false, true),
|
||||
StatescriptVariableType.Decimal => new NumericConfig(-1e10, 1e10, 0.001, false, true),
|
||||
StatescriptVariableType.Bool => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Vector2 => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Vector3 => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Vector4 => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Plane => throw new NotImplementedException(),
|
||||
StatescriptVariableType.Quaternion => throw new NotImplementedException(),
|
||||
_ => new NumericConfig(-1e10, 1e10, 0.001, false, true),
|
||||
};
|
||||
}
|
||||
|
||||
private readonly record struct NumericConfig(
|
||||
double MinValue,
|
||||
double MaxValue,
|
||||
double Step,
|
||||
bool IsInteger,
|
||||
bool AllowBeyondRange);
|
||||
|
||||
private static StyleBox GetPanelStyle()
|
||||
{
|
||||
if (_cachedPanelStyle is null || !GodotObject.IsInstanceValid(_cachedPanelStyle))
|
||||
{
|
||||
Control baseControl = EditorInterface.Singleton.GetBaseControl();
|
||||
_cachedPanelStyle = (StyleBox)baseControl.GetThemeStylebox("normal", "LineEdit").Duplicate();
|
||||
_cachedPanelStyle.SetContentMarginAll(0);
|
||||
}
|
||||
|
||||
return _cachedPanelStyle;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpoji1y5vib4o
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
public partial class StatescriptGraphEditorDock
|
||||
{
|
||||
private void OnFileMenuIdPressed(long id)
|
||||
{
|
||||
switch ((int)id)
|
||||
{
|
||||
case 0:
|
||||
ShowNewStatescriptDialog();
|
||||
break;
|
||||
|
||||
case 1:
|
||||
ShowLoadStatescriptDialog();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
OnSavePressed();
|
||||
break;
|
||||
|
||||
case 3:
|
||||
ShowSaveAsDialog();
|
||||
break;
|
||||
|
||||
case 4:
|
||||
CloseCurrentTab();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNewStatescriptDialog()
|
||||
{
|
||||
_newStatescriptDialog?.QueueFree();
|
||||
|
||||
_newStatescriptDialog = new AcceptDialog
|
||||
{
|
||||
Title = "Create Statescript",
|
||||
Size = new Vector2I(400, 140),
|
||||
Exclusive = true,
|
||||
};
|
||||
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
var pathRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(pathRow);
|
||||
|
||||
pathRow.AddChild(new Label { Text = "Path:", CustomMinimumSize = new Vector2(50, 0) });
|
||||
|
||||
_newStatescriptPathEdit = new LineEdit
|
||||
{
|
||||
Text = "res://new_statescript.tres",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
pathRow.AddChild(_newStatescriptPathEdit);
|
||||
|
||||
_newStatescriptDialog.AddChild(vBox);
|
||||
_newStatescriptDialog.Confirmed += OnNewStatescriptConfirmed;
|
||||
|
||||
AddChild(_newStatescriptDialog);
|
||||
_newStatescriptDialog.PopupCentered();
|
||||
}
|
||||
|
||||
private void OnNewStatescriptConfirmed()
|
||||
{
|
||||
if (_newStatescriptPathEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = _newStatescriptPathEdit.Text.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.EndsWith(".tres", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path += ".tres";
|
||||
}
|
||||
|
||||
var graph = new StatescriptGraph();
|
||||
graph.EnsureEntryNode();
|
||||
|
||||
graph.StatescriptName = path.GetFile().GetBaseName();
|
||||
|
||||
ResourceSaver.Save(graph, path);
|
||||
EditorInterface.Singleton.GetResourceFilesystem().Scan();
|
||||
|
||||
graph = ResourceLoader.Load<StatescriptGraph>(path);
|
||||
if (graph is not null)
|
||||
{
|
||||
OpenGraph(graph);
|
||||
}
|
||||
|
||||
_newStatescriptDialog?.QueueFree();
|
||||
_newStatescriptDialog = null;
|
||||
}
|
||||
|
||||
private void ShowLoadStatescriptDialog()
|
||||
{
|
||||
var dialog = new EditorFileDialog
|
||||
{
|
||||
FileMode = FileDialog.FileModeEnum.OpenFile,
|
||||
Title = "Load Statescript File",
|
||||
Access = FileDialog.AccessEnum.Resources,
|
||||
};
|
||||
|
||||
dialog.AddFilter("*.tres;StatescriptGraph");
|
||||
dialog.FileSelected += path =>
|
||||
{
|
||||
Resource? graph = ResourceLoader.Load(path);
|
||||
if (graph is StatescriptGraph statescriptGraph)
|
||||
{
|
||||
OpenGraph(statescriptGraph);
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PushWarning($"Failed to load StatescriptGraph from: {path}");
|
||||
}
|
||||
|
||||
dialog.QueueFree();
|
||||
};
|
||||
|
||||
dialog.Canceled += dialog.QueueFree;
|
||||
|
||||
AddChild(dialog);
|
||||
dialog.PopupCentered(new Vector2I(700, 500));
|
||||
}
|
||||
|
||||
private void ShowSaveAsDialog()
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new EditorFileDialog
|
||||
{
|
||||
FileMode = FileDialog.FileModeEnum.SaveFile,
|
||||
Title = "Save Statescript As",
|
||||
Access = FileDialog.AccessEnum.Resources,
|
||||
};
|
||||
|
||||
dialog.AddFilter("*.tres", "Godot Resource");
|
||||
dialog.FileSelected += path =>
|
||||
{
|
||||
if (_graphEdit is not null)
|
||||
{
|
||||
graph.ScrollOffset = _graphEdit.ScrollOffset;
|
||||
graph.Zoom = _graphEdit.Zoom;
|
||||
SyncVisualNodePositionsToGraph();
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
|
||||
ResourceSaver.Save(graph, path);
|
||||
EditorInterface.Singleton.GetResourceFilesystem().Scan();
|
||||
GD.Print($"Statescript graph saved as: {path}");
|
||||
|
||||
StatescriptGraph? savedGraph = ResourceLoader.Load<StatescriptGraph>(path);
|
||||
if (savedGraph is not null)
|
||||
{
|
||||
OpenGraph(savedGraph);
|
||||
}
|
||||
|
||||
dialog.QueueFree();
|
||||
};
|
||||
|
||||
dialog.Canceled += dialog.QueueFree;
|
||||
|
||||
AddChild(dialog);
|
||||
dialog.PopupCentered(new Vector2I(700, 500));
|
||||
}
|
||||
|
||||
private void OnSavePressed()
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
graph.ScrollOffset = _graphEdit.ScrollOffset;
|
||||
graph.Zoom = _graphEdit.Zoom;
|
||||
SyncVisualNodePositionsToGraph();
|
||||
SyncConnectionsToCurrentGraph();
|
||||
|
||||
if (string.IsNullOrEmpty(graph.ResourcePath))
|
||||
{
|
||||
ShowSaveAsDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
SaveGraphResource(graph);
|
||||
GD.Print($"Statescript graph saved: {graph.ResourcePath}");
|
||||
}
|
||||
|
||||
private void OnGraphEditPopupRequest(Vector2 atPosition)
|
||||
{
|
||||
if (CurrentGraph is null || _graphEdit is null || _addNodeDialog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearPendingConnection();
|
||||
|
||||
Vector2 graphPosition = (_graphEdit.ScrollOffset + atPosition) / _graphEdit.Zoom;
|
||||
var screenPosition = (Vector2I)(_graphEdit.GetScreenPosition() + atPosition);
|
||||
|
||||
_addNodeDialog.ShowAtPosition(graphPosition, screenPosition);
|
||||
}
|
||||
|
||||
private void OnConnectionToEmpty(StringName fromNode, long fromPort, Vector2 releasePosition)
|
||||
{
|
||||
if (CurrentGraph is null || _graphEdit is null || _addNodeDialog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingConnectionNode = fromNode;
|
||||
_pendingConnectionPort = (int)fromPort;
|
||||
_pendingConnectionIsOutput = true;
|
||||
|
||||
Vector2 graphPosition = (_graphEdit.ScrollOffset + releasePosition) / _graphEdit.Zoom;
|
||||
var screenPosition = (Vector2I)(_graphEdit.GetScreenPosition() + releasePosition);
|
||||
|
||||
_addNodeDialog.ShowAtPosition(graphPosition, screenPosition);
|
||||
}
|
||||
|
||||
private void OnConnectionFromEmpty(StringName toNode, long toPort, Vector2 releasePosition)
|
||||
{
|
||||
if (CurrentGraph is null || _graphEdit is null || _addNodeDialog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingConnectionNode = toNode;
|
||||
_pendingConnectionPort = (int)toPort;
|
||||
_pendingConnectionIsOutput = false;
|
||||
|
||||
Vector2 graphPosition = (_graphEdit.ScrollOffset + releasePosition) / _graphEdit.Zoom;
|
||||
var screenPosition = (Vector2I)(_graphEdit.GetScreenPosition() + releasePosition);
|
||||
|
||||
_addNodeDialog.ShowAtPosition(graphPosition, screenPosition);
|
||||
}
|
||||
|
||||
private void OnDialogNodeCreationRequested(
|
||||
StatescriptNodeDiscovery.NodeTypeInfo? typeInfo,
|
||||
StatescriptNodeType nodeType,
|
||||
Vector2 position)
|
||||
{
|
||||
string newNodeId;
|
||||
|
||||
if (typeInfo is not null)
|
||||
{
|
||||
newNodeId = AddNodeAtPosition(nodeType, typeInfo.DisplayName, typeInfo.RuntimeTypeName, position);
|
||||
}
|
||||
else
|
||||
{
|
||||
newNodeId = AddNodeAtPosition(StatescriptNodeType.Exit, "Exit", string.Empty, position);
|
||||
}
|
||||
|
||||
if (_pendingConnectionNode is not null && _graphEdit is not null)
|
||||
{
|
||||
if (_pendingConnectionIsOutput)
|
||||
{
|
||||
var inputPort = FindFirstEnabledInputPort(newNodeId);
|
||||
if (inputPort >= 0)
|
||||
{
|
||||
OnConnectionRequest(
|
||||
_pendingConnectionNode,
|
||||
_pendingConnectionPort,
|
||||
newNodeId,
|
||||
inputPort);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var outputPort = FindFirstEnabledOutputPort(newNodeId);
|
||||
if (outputPort >= 0)
|
||||
{
|
||||
OnConnectionRequest(
|
||||
newNodeId,
|
||||
outputPort,
|
||||
_pendingConnectionNode,
|
||||
_pendingConnectionPort);
|
||||
}
|
||||
}
|
||||
|
||||
ClearPendingConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDialogCanceled()
|
||||
{
|
||||
ClearPendingConnection();
|
||||
}
|
||||
|
||||
private void ClearPendingConnection()
|
||||
{
|
||||
_pendingConnectionNode = null;
|
||||
_pendingConnectionPort = 0;
|
||||
_pendingConnectionIsOutput = false;
|
||||
}
|
||||
|
||||
private void OnAddNodeButtonPressed()
|
||||
{
|
||||
if (CurrentGraph is null || _graphEdit is null || _addNodeDialog is null || _addNodeButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearPendingConnection();
|
||||
|
||||
var screenPosition = (Vector2I)(_addNodeButton.GetScreenPosition() + new Vector2(0, _addNodeButton.Size.Y));
|
||||
|
||||
Vector2 centerPosition = (_graphEdit.ScrollOffset + (_graphEdit.Size / 2)) / _graphEdit.Zoom;
|
||||
|
||||
_addNodeDialog.ShowAtPosition(centerPosition, screenPosition);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://drxix8xbwpfin
|
||||
@@ -0,0 +1,493 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
using GodotCollections = Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
public partial class StatescriptGraphEditorDock
|
||||
{
|
||||
private static bool WouldCreateLoop(
|
||||
StatescriptGraph graphResource,
|
||||
string fromNodeId,
|
||||
int fromPort,
|
||||
string toNodeId,
|
||||
int toPort)
|
||||
{
|
||||
var tempConnection = new StatescriptConnection
|
||||
{
|
||||
FromNode = fromNodeId,
|
||||
OutputPort = fromPort,
|
||||
ToNode = toNodeId,
|
||||
InputPort = toPort,
|
||||
};
|
||||
|
||||
graphResource.Connections.Add(tempConnection);
|
||||
|
||||
try
|
||||
{
|
||||
StatescriptGraphBuilder.Build(graphResource);
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
graphResource.Connections.Remove(tempConnection);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnConnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort)
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WouldCreateLoop(graph, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort))
|
||||
{
|
||||
ShowLoopWarningDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Connect Statescript Nodes", customContext: graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoConnect,
|
||||
fromNode.ToString(),
|
||||
(int)fromPort,
|
||||
toNode.ToString(),
|
||||
(int)toPort);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.UndoConnect,
|
||||
fromNode.ToString(),
|
||||
(int)fromPort,
|
||||
toNode.ToString(),
|
||||
(int)toPort);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoConnect(fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort);
|
||||
}
|
||||
}
|
||||
|
||||
private void DoConnect(string fromNode, int fromPort, string toNode, int toPort)
|
||||
{
|
||||
_graphEdit?.ConnectNode(fromNode, fromPort, toNode, toPort);
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
|
||||
private void UndoConnect(string fromNode, int fromPort, string toNode, int toPort)
|
||||
{
|
||||
_graphEdit?.DisconnectNode(fromNode, fromPort, toNode, toPort);
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
|
||||
private void OnDisconnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort)
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Disconnect Statescript Nodes", customContext: graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.UndoConnect,
|
||||
fromNode.ToString(),
|
||||
(int)fromPort,
|
||||
toNode.ToString(),
|
||||
(int)toPort);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.DoConnect,
|
||||
fromNode.ToString(),
|
||||
(int)fromPort,
|
||||
toNode.ToString(),
|
||||
(int)toPort);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
_graphEdit.DisconnectNode(fromNode, (int)fromPort, toNode, (int)toPort);
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeleteNodesRequest(GodotCollections.Array<StringName> deletedNodes)
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (StringName nodeName in deletedNodes)
|
||||
{
|
||||
Node? child = _graphEdit.GetNodeOrNull(nodeName.ToString());
|
||||
|
||||
if (child is not StatescriptGraphNode graphNode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (graphNode.NodeResource?.NodeType == StatescriptNodeType.Entry)
|
||||
{
|
||||
GD.PushWarning("Cannot delete the Entry statescriptNode.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (graphNode.NodeResource is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var affectedConnections = new List<StatescriptConnection>();
|
||||
foreach (GodotCollections.Dictionary connection in _graphEdit.GetConnectionList())
|
||||
{
|
||||
StringName from = connection["from_node"].AsStringName();
|
||||
StringName to = connection["to_node"].AsStringName();
|
||||
|
||||
if (from == nodeName || to == nodeName)
|
||||
{
|
||||
affectedConnections.Add(new StatescriptConnection
|
||||
{
|
||||
FromNode = connection["from_node"].AsString(),
|
||||
OutputPort = connection["from_port"].AsInt32(),
|
||||
ToNode = connection["to_node"].AsString(),
|
||||
InputPort = connection["to_port"].AsInt32(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Delete Statescript Node", customContext: graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoDeleteNode,
|
||||
graph,
|
||||
graphNode.NodeResource,
|
||||
new GodotCollections.Array<StatescriptConnection>(affectedConnections));
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.UndoDeleteNode,
|
||||
graph,
|
||||
graphNode.NodeResource,
|
||||
new GodotCollections.Array<StatescriptConnection>(affectedConnections));
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoDeleteNode(
|
||||
graph,
|
||||
graphNode.NodeResource,
|
||||
[.. affectedConnections]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DoDeleteNode(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNode nodeResource,
|
||||
GodotCollections.Array<StatescriptConnection> affectedConnections)
|
||||
{
|
||||
if (_graphEdit is not null && CurrentGraph == graph)
|
||||
{
|
||||
foreach (StatescriptConnection connection in affectedConnections)
|
||||
{
|
||||
_graphEdit.DisconnectNode(
|
||||
connection.FromNode,
|
||||
connection.OutputPort,
|
||||
connection.ToNode,
|
||||
connection.InputPort);
|
||||
}
|
||||
|
||||
Node? child = _graphEdit.GetNodeOrNull(nodeResource.NodeId);
|
||||
child?.QueueFree();
|
||||
}
|
||||
|
||||
graph.Nodes.Remove(nodeResource);
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
|
||||
private void UndoDeleteNode(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNode nodeResource,
|
||||
GodotCollections.Array<StatescriptConnection> affectedConnections)
|
||||
{
|
||||
graph.Nodes.Add(nodeResource);
|
||||
|
||||
graph.Connections.AddRange(affectedConnections);
|
||||
|
||||
if (CurrentGraph == graph)
|
||||
{
|
||||
LoadGraphIntoEditor(graph);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeginNodeMove()
|
||||
{
|
||||
if (_graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_preMovePositions.Clear();
|
||||
foreach (Node child in _graphEdit.GetChildren())
|
||||
{
|
||||
if (child is StatescriptGraphNode { Selected: true } sgn)
|
||||
{
|
||||
_preMovePositions[sgn.Name] = sgn.PositionOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEndNodeMove()
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null || _preMovePositions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var movedNodes = new GodotCollections.Dictionary<StringName, Vector2>();
|
||||
var oldPositions = new GodotCollections.Dictionary<StringName, Vector2>();
|
||||
|
||||
foreach (Node child in _graphEdit.GetChildren())
|
||||
{
|
||||
if (child is not StatescriptGraphNode sgn || !_preMovePositions.TryGetValue(sgn.Name, out Vector2 oldPos))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 newPos = sgn.PositionOffset;
|
||||
if (oldPos != newPos)
|
||||
{
|
||||
movedNodes[sgn.Name] = newPos;
|
||||
oldPositions[sgn.Name] = oldPos;
|
||||
}
|
||||
}
|
||||
|
||||
_preMovePositions.Clear();
|
||||
|
||||
if (movedNodes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Move Statescript Node(s)", customContext: graph);
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoMoveNodes, graph, movedNodes);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.DoMoveNodes, graph, oldPositions);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
SyncNodePositionsToResource(graph, movedNodes);
|
||||
}
|
||||
|
||||
private void DoMoveNodes(
|
||||
StatescriptGraph graph,
|
||||
GodotCollections.Dictionary<StringName, Vector2> positions)
|
||||
{
|
||||
foreach (StatescriptNode node in graph.Nodes)
|
||||
{
|
||||
if (positions.TryGetValue(node.NodeId, out Vector2 pos))
|
||||
{
|
||||
node.PositionOffset = pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentGraph == graph && _graphEdit is not null)
|
||||
{
|
||||
foreach (Node child in _graphEdit.GetChildren())
|
||||
{
|
||||
if (child is StatescriptGraphNode sgn && positions.TryGetValue(sgn.Name, out Vector2 pos))
|
||||
{
|
||||
sgn.PositionOffset = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string AddNodeAtPosition(
|
||||
StatescriptNodeType nodeType,
|
||||
string title,
|
||||
string runtimeTypeName,
|
||||
Vector2 position)
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var nodeId = $"node_{_nextNodeId++}";
|
||||
|
||||
var nodeResource = new StatescriptNode
|
||||
{
|
||||
NodeId = nodeId,
|
||||
Title = title,
|
||||
NodeType = nodeType,
|
||||
RuntimeTypeName = runtimeTypeName,
|
||||
PositionOffset = position,
|
||||
};
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Add Statescript Node", customContext: graph);
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoAddNode, graph, nodeResource);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoAddNode, graph, nodeResource);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoAddNode(graph, nodeResource);
|
||||
}
|
||||
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
private void DoAddNode(StatescriptGraph graph, StatescriptNode nodeResource)
|
||||
{
|
||||
graph.Nodes.Add(nodeResource);
|
||||
|
||||
if (CurrentGraph == graph && _graphEdit is not null)
|
||||
{
|
||||
var graphNode = new StatescriptGraphNode();
|
||||
_graphEdit.AddChild(graphNode);
|
||||
graphNode.Initialize(nodeResource, graph);
|
||||
graphNode.SetUndoRedo(_undoRedo);
|
||||
}
|
||||
}
|
||||
|
||||
private void UndoAddNode(StatescriptGraph graph, StatescriptNode nodeResource)
|
||||
{
|
||||
graph.Nodes.Remove(nodeResource);
|
||||
|
||||
if (CurrentGraph == graph)
|
||||
{
|
||||
LoadGraphIntoEditor(graph);
|
||||
}
|
||||
}
|
||||
|
||||
private void DuplicateSelectedNodes()
|
||||
{
|
||||
StatescriptGraph? graph = CurrentGraph;
|
||||
if (graph is null || _graphEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedNodes = new List<StatescriptGraphNode>();
|
||||
|
||||
foreach (Node child in _graphEdit.GetChildren())
|
||||
{
|
||||
if (child is StatescriptGraphNode { Selected: true } statescriptNode
|
||||
&& statescriptNode.NodeResource is not null
|
||||
&& statescriptNode.NodeResource.NodeType != StatescriptNodeType.Entry)
|
||||
{
|
||||
selectedNodes.Add(statescriptNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedNodes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (StatescriptGraphNode sgn in selectedNodes)
|
||||
{
|
||||
sgn.Selected = false;
|
||||
}
|
||||
|
||||
var duplicatedIds = new Dictionary<string, string>();
|
||||
const float offset = 40f;
|
||||
|
||||
foreach (StatescriptGraphNode sgn in selectedNodes)
|
||||
{
|
||||
StatescriptNode original = sgn.NodeResource!;
|
||||
var newNodeId = $"node_{_nextNodeId++}";
|
||||
duplicatedIds[original.NodeId] = newNodeId;
|
||||
|
||||
var duplicated = new StatescriptNode
|
||||
{
|
||||
NodeId = newNodeId,
|
||||
Title = original.Title,
|
||||
NodeType = original.NodeType,
|
||||
RuntimeTypeName = original.RuntimeTypeName,
|
||||
PositionOffset = original.PositionOffset + new Vector2(offset, offset),
|
||||
};
|
||||
|
||||
foreach (KeyValuePair<string, Variant> kvp in original.CustomData)
|
||||
{
|
||||
duplicated.CustomData[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
foreach (StatescriptNodeProperty binding in original.PropertyBindings)
|
||||
{
|
||||
var newBinding = new StatescriptNodeProperty
|
||||
{
|
||||
Direction = binding.Direction,
|
||||
PropertyIndex = binding.PropertyIndex,
|
||||
Resolver = binding.Resolver is not null
|
||||
? (StatescriptResolverResource)binding.Resolver.Duplicate(true)
|
||||
: null,
|
||||
};
|
||||
|
||||
duplicated.PropertyBindings.Add(newBinding);
|
||||
}
|
||||
|
||||
graph.Nodes.Add(duplicated);
|
||||
|
||||
var graphNode = new StatescriptGraphNode();
|
||||
_graphEdit.AddChild(graphNode);
|
||||
graphNode.Initialize(duplicated, graph);
|
||||
graphNode.Selected = true;
|
||||
}
|
||||
|
||||
foreach (StatescriptConnection connection in graph.Connections)
|
||||
{
|
||||
if (duplicatedIds.TryGetValue(connection.FromNode, out var newFrom)
|
||||
&& duplicatedIds.TryGetValue(connection.ToNode, out var newTo))
|
||||
{
|
||||
_graphEdit.ConnectNode(newFrom, connection.OutputPort, newTo, connection.InputPort);
|
||||
}
|
||||
}
|
||||
|
||||
SyncConnectionsToCurrentGraph();
|
||||
}
|
||||
|
||||
private void ShowLoopWarningDialog()
|
||||
{
|
||||
var dialog = new AcceptDialog
|
||||
{
|
||||
Title = "Connection Rejected",
|
||||
DialogText = "This connection would create a loop in the graph, which is not allowed.",
|
||||
Exclusive = true,
|
||||
};
|
||||
|
||||
dialog.Confirmed += dialog.QueueFree;
|
||||
dialog.Canceled += dialog.QueueFree;
|
||||
AddChild(dialog);
|
||||
dialog.PopupCentered();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0pse6qnrsdg0
|
||||
1420
addons/forge/editor/statescript/StatescriptGraphEditorDock.cs
Normal file
1420
addons/forge/editor/statescript/StatescriptGraphEditorDock.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://1mt1aejs15yr
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
public partial class StatescriptGraphNode
|
||||
{
|
||||
private bool ReferencesVariable(string variableName)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (StatescriptNodeProperty binding in NodeResource.PropertyBindings)
|
||||
{
|
||||
if (binding.Resolver is VariableResolverResource varRes
|
||||
&& varRes.VariableName == variableName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyHighlightBorder()
|
||||
{
|
||||
if (_isHighlighted)
|
||||
{
|
||||
if (GetThemeStylebox("panel") is not StyleBoxFlat baseStyle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var highlightStyle = (StyleBoxFlat)baseStyle.Duplicate();
|
||||
|
||||
highlightStyle.BorderColor = _highlightColor;
|
||||
highlightStyle.BorderWidthTop = 2;
|
||||
highlightStyle.BorderWidthBottom = 2;
|
||||
highlightStyle.BorderWidthLeft = 2;
|
||||
highlightStyle.BorderWidthRight = 2;
|
||||
|
||||
highlightStyle.BgColor = baseStyle.BgColor.Lerp(_highlightColor, 0.15f);
|
||||
|
||||
AddThemeStyleboxOverride("panel", highlightStyle);
|
||||
AddThemeStyleboxOverride("panel_selected", highlightStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveThemeStyleboxOverride("panel");
|
||||
RemoveThemeStyleboxOverride("panel_selected");
|
||||
|
||||
ApplyBottomPadding();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateChildHighlights()
|
||||
{
|
||||
UpdateHighlightsRecursive(this);
|
||||
}
|
||||
|
||||
private void UpdateHighlightsRecursive(Node parent)
|
||||
{
|
||||
foreach (Node child in parent.GetChildren())
|
||||
{
|
||||
if (child is OptionButton optionButton)
|
||||
{
|
||||
HighlightOptionButtonIfMatches(optionButton);
|
||||
}
|
||||
else if (child is Label label)
|
||||
{
|
||||
HighlightLabelIfMatches(label);
|
||||
}
|
||||
|
||||
UpdateHighlightsRecursive(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void HighlightOptionButtonIfMatches(OptionButton dropdown)
|
||||
{
|
||||
if (!dropdown.HasMeta("is_variable_dropdown"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_highlightedVariableName))
|
||||
{
|
||||
dropdown.RemoveThemeStyleboxOverride("normal");
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedIdx = dropdown.Selected;
|
||||
if (selectedIdx < 0)
|
||||
{
|
||||
dropdown.RemoveThemeStyleboxOverride("normal");
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedText = dropdown.GetItemText(selectedIdx);
|
||||
if (selectedText == _highlightedVariableName)
|
||||
{
|
||||
if (dropdown.GetThemeStylebox("normal") is not StyleBoxFlat baseStyle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var highlightStyle = (StyleBoxFlat)baseStyle.Duplicate();
|
||||
|
||||
highlightStyle.BgColor = baseStyle.BgColor.Lerp(_highlightColor, 0.25f);
|
||||
|
||||
dropdown.AddThemeStyleboxOverride("normal", highlightStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
dropdown.RemoveThemeStyleboxOverride("normal");
|
||||
}
|
||||
}
|
||||
|
||||
private void HighlightLabelIfMatches(Label label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_highlightedVariableName))
|
||||
{
|
||||
if (label.HasMeta("is_highlight_colored"))
|
||||
{
|
||||
label.RemoveThemeColorOverride("font_color");
|
||||
label.RemoveMeta("is_highlight_colored");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (label.Text == _highlightedVariableName)
|
||||
{
|
||||
label.AddThemeColorOverride("font_color", _highlightColor);
|
||||
label.SetMeta("is_highlight_colored", true);
|
||||
}
|
||||
else if (label.HasMeta("is_highlight_colored"))
|
||||
{
|
||||
label.RemoveThemeColorOverride("font_color");
|
||||
label.RemoveMeta("is_highlight_colored");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmp3vltauax62
|
||||
@@ -0,0 +1,221 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
public partial class StatescriptGraphNode
|
||||
{
|
||||
private void SetupNodeByType(StatescriptNodeType nodeType)
|
||||
{
|
||||
switch (nodeType)
|
||||
{
|
||||
case StatescriptNodeType.Entry:
|
||||
SetupEntryNode();
|
||||
break;
|
||||
case StatescriptNodeType.Exit:
|
||||
SetupExitNode();
|
||||
break;
|
||||
case StatescriptNodeType.Action:
|
||||
SetupActionNode();
|
||||
break;
|
||||
case StatescriptNodeType.Condition:
|
||||
SetupConditionNode();
|
||||
break;
|
||||
case StatescriptNodeType.State:
|
||||
SetupStateNode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupEntryNode()
|
||||
{
|
||||
CustomMinimumSize = new Vector2(100, 0);
|
||||
|
||||
var label = new Label { Text = "Start" };
|
||||
AddChild(label);
|
||||
SetSlotEnabledRight(0, true);
|
||||
SetSlotColorRight(0, _eventColor);
|
||||
|
||||
ApplyTitleBarColor(_entryColor);
|
||||
}
|
||||
|
||||
private void SetupExitNode()
|
||||
{
|
||||
CustomMinimumSize = new Vector2(100, 0);
|
||||
|
||||
var label = new Label { Text = "End" };
|
||||
AddChild(label);
|
||||
SetSlotEnabledLeft(0, true);
|
||||
SetSlotColorLeft(0, _eventColor);
|
||||
|
||||
ApplyTitleBarColor(_exitColor);
|
||||
}
|
||||
|
||||
private void SetupActionNode()
|
||||
{
|
||||
var label = new Label { Text = "Execute" };
|
||||
AddChild(label);
|
||||
SetSlotEnabledLeft(0, true);
|
||||
SetSlotColorLeft(0, _eventColor);
|
||||
SetSlotEnabledRight(0, true);
|
||||
SetSlotColorRight(0, _eventColor);
|
||||
|
||||
ApplyTitleBarColor(_actionColor);
|
||||
}
|
||||
|
||||
private void SetupConditionNode()
|
||||
{
|
||||
var hBox = new HBoxContainer();
|
||||
hBox.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(hBox);
|
||||
|
||||
var inputLabel = new Label { Text = "Condition" };
|
||||
hBox.AddChild(inputLabel);
|
||||
SetSlotEnabledLeft(0, true);
|
||||
SetSlotColorLeft(0, _eventColor);
|
||||
|
||||
var trueLabel = new Label
|
||||
{
|
||||
Text = "True",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
hBox.AddChild(trueLabel);
|
||||
SetSlotEnabledRight(0, true);
|
||||
SetSlotColorRight(0, _eventColor);
|
||||
|
||||
var falseLabel = new Label
|
||||
{
|
||||
Text = "False",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(falseLabel);
|
||||
SetSlotEnabledRight(1, true);
|
||||
SetSlotColorRight(1, _eventColor);
|
||||
ApplyTitleBarColor(_conditionColor);
|
||||
}
|
||||
|
||||
private void SetupStateNode()
|
||||
{
|
||||
var hBox1 = new HBoxContainer();
|
||||
hBox1.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(hBox1);
|
||||
|
||||
var inputLabel = new Label { Text = "Begin" };
|
||||
hBox1.AddChild(inputLabel);
|
||||
SetSlotEnabledLeft(0, true);
|
||||
SetSlotColorLeft(0, _eventColor);
|
||||
|
||||
var activateLabel = new Label
|
||||
{
|
||||
Text = "OnActivate",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
hBox1.AddChild(activateLabel);
|
||||
SetSlotEnabledRight(0, true);
|
||||
SetSlotColorRight(0, _eventColor);
|
||||
|
||||
var hBox2 = new HBoxContainer();
|
||||
hBox2.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(hBox2);
|
||||
|
||||
var abortLabel = new Label { Text = "Abort" };
|
||||
hBox2.AddChild(abortLabel);
|
||||
SetSlotEnabledLeft(1, true);
|
||||
SetSlotColorLeft(1, _eventColor);
|
||||
|
||||
var deactivateLabel = new Label
|
||||
{
|
||||
Text = "OnDeactivate",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
hBox2.AddChild(deactivateLabel);
|
||||
SetSlotEnabledRight(1, true);
|
||||
SetSlotColorRight(1, _eventColor);
|
||||
|
||||
var abortOutputLabel = new Label
|
||||
{
|
||||
Text = "OnAbort",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(abortOutputLabel);
|
||||
SetSlotEnabledRight(2, true);
|
||||
SetSlotColorRight(2, _eventColor);
|
||||
|
||||
var subgraphLabel = new Label
|
||||
{
|
||||
Text = "Subgraph",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(subgraphLabel);
|
||||
SetSlotEnabledRight(3, true);
|
||||
SetSlotColorRight(3, _subgraphColor);
|
||||
|
||||
ApplyTitleBarColor(_stateColor);
|
||||
}
|
||||
|
||||
private void ClearSlots()
|
||||
{
|
||||
foreach (Node child in GetChildren())
|
||||
{
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTitleBarColor(Color color)
|
||||
{
|
||||
var titleBarStyleBox = new StyleBoxFlat
|
||||
{
|
||||
BgColor = color,
|
||||
ContentMarginLeft = 12,
|
||||
ContentMarginRight = 12,
|
||||
ContentMarginTop = 6,
|
||||
ContentMarginBottom = 6,
|
||||
CornerRadiusTopLeft = 4,
|
||||
CornerRadiusTopRight = 4,
|
||||
};
|
||||
|
||||
AddThemeStyleboxOverride("titlebar", titleBarStyleBox);
|
||||
|
||||
var selectedTitleBarStyleBox = (StyleBoxFlat)titleBarStyleBox.Duplicate();
|
||||
selectedTitleBarStyleBox.BgColor = color.Lightened(0.2f);
|
||||
AddThemeStyleboxOverride("titlebar_selected", selectedTitleBarStyleBox);
|
||||
}
|
||||
|
||||
private void ApplyBottomPadding()
|
||||
{
|
||||
StyleBox? existing = GetThemeStylebox("panel");
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
var panelStyle = (StyleBox)existing.Duplicate();
|
||||
panelStyle.ContentMarginBottom = 10;
|
||||
AddThemeStyleboxOverride("panel", panelStyle);
|
||||
}
|
||||
|
||||
StyleBox? selectedExisting = GetThemeStylebox("panel_selected");
|
||||
|
||||
if (selectedExisting is not null)
|
||||
{
|
||||
var selectedPanelStyle = (StyleBox)selectedExisting.Duplicate();
|
||||
selectedPanelStyle.ContentMarginBottom = 10;
|
||||
AddThemeStyleboxOverride("panel_selected", selectedPanelStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://civ3te4ediqxn
|
||||
@@ -0,0 +1,380 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
public partial class StatescriptGraphNode
|
||||
{
|
||||
private readonly Dictionary<PropertySlotKey, InputPropertyContext> _inputPropertyContexts = [];
|
||||
|
||||
private void AddInputPropertyRow(
|
||||
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
|
||||
int index,
|
||||
Control sectionContainer)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var container = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
sectionContainer.AddChild(container);
|
||||
|
||||
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
container.AddChild(headerRow);
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = propInfo.Label,
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
};
|
||||
|
||||
nameLabel.AddThemeColorOverride("font_color", _inputPropertyColor);
|
||||
headerRow.AddChild(nameLabel);
|
||||
|
||||
List<Func<NodeEditorProperty>> resolverFactories =
|
||||
StatescriptResolverRegistry.GetCompatibleFactories(propInfo.ExpectedType);
|
||||
|
||||
if (resolverFactories.Count == 0)
|
||||
{
|
||||
var errorLabel = new Label
|
||||
{
|
||||
Text = "No compatible resolvers.",
|
||||
};
|
||||
|
||||
errorLabel.AddThemeColorOverride("font_color", Colors.Red);
|
||||
headerRow.AddChild(errorLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
var resolverDropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(80, 0),
|
||||
};
|
||||
|
||||
foreach (Func<NodeEditorProperty> factory in resolverFactories)
|
||||
{
|
||||
using NodeEditorProperty temp = factory();
|
||||
resolverDropdown.AddItem(temp.DisplayName);
|
||||
}
|
||||
|
||||
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Input, index);
|
||||
var selectedIndex = 0;
|
||||
|
||||
if (binding?.Resolver is not null)
|
||||
{
|
||||
for (var i = 0; i < resolverFactories.Count; i++)
|
||||
{
|
||||
using NodeEditorProperty temp = resolverFactories[i]();
|
||||
|
||||
if (temp.ResolverTypeId == GetResolverTypeId(binding.Resolver))
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < resolverFactories.Count; i++)
|
||||
{
|
||||
using NodeEditorProperty temp = resolverFactories[i]();
|
||||
|
||||
if (temp.ResolverTypeId == "Variant")
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolverDropdown.Selected = selectedIndex;
|
||||
headerRow.AddChild(resolverDropdown);
|
||||
|
||||
var editorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
container.AddChild(editorContainer);
|
||||
|
||||
var key = new PropertySlotKey(StatescriptPropertyDirection.Input, index);
|
||||
_inputPropertyContexts[key] = new InputPropertyContext(resolverFactories, propInfo, editorContainer);
|
||||
|
||||
ShowResolverEditorUI(
|
||||
resolverFactories[selectedIndex],
|
||||
binding,
|
||||
propInfo.ExpectedType,
|
||||
editorContainer,
|
||||
StatescriptPropertyDirection.Input,
|
||||
index,
|
||||
propInfo.IsArray);
|
||||
|
||||
var capturedIndex = index;
|
||||
resolverDropdown.ItemSelected += selectedItem => OnInputResolverDropdownItemSelected(selectedItem, capturedIndex);
|
||||
}
|
||||
|
||||
private void OnInputResolverDropdownItemSelected(long x, int index)
|
||||
{
|
||||
var key = new PropertySlotKey(StatescriptPropertyDirection.Input, index);
|
||||
|
||||
if (!_inputPropertyContexts.TryGetValue(key, out InputPropertyContext? ctx))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldResolver = FindBinding(StatescriptPropertyDirection.Input, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
if (_activeResolverEditors.TryGetValue(key, out NodeEditorProperty? old))
|
||||
{
|
||||
_activeResolverEditors.Remove(key);
|
||||
}
|
||||
|
||||
ClearContainer(ctx.EditorContainer);
|
||||
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ShowResolverEditorUI(
|
||||
ctx.ResolverFactories[(int)x],
|
||||
null,
|
||||
ctx.PropInfo.ExpectedType,
|
||||
ctx.EditorContainer,
|
||||
StatescriptPropertyDirection.Input,
|
||||
index,
|
||||
ctx.PropInfo.IsArray);
|
||||
|
||||
if (_activeResolverEditors.TryGetValue(key, out NodeEditorProperty? editor))
|
||||
{
|
||||
SaveResolverEditor(editor, StatescriptPropertyDirection.Input, index);
|
||||
}
|
||||
|
||||
StatescriptNodeProperty? updated = FindBinding(StatescriptPropertyDirection.Input, index);
|
||||
var newResolver = updated?.Resolver?.Duplicate() as StatescriptResolverResource;
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Change Resolver Type", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)StatescriptPropertyDirection.Input,
|
||||
index,
|
||||
newResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)StatescriptPropertyDirection.Input,
|
||||
index,
|
||||
oldResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
PropertyBindingChanged?.Invoke();
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private void AddOutputVariableRow(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
FoldableContainer sectionContainer)
|
||||
{
|
||||
if (NodeResource is null || _graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
sectionContainer.AddChild(hBox);
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = varInfo.Label,
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
};
|
||||
|
||||
nameLabel.AddThemeColorOverride("font_color", _outputVariableColor);
|
||||
hBox.AddChild(nameLabel);
|
||||
|
||||
var variableDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
variableDropdown.SetMeta("is_variable_dropdown", true);
|
||||
variableDropdown.SetMeta("output_index", index);
|
||||
|
||||
foreach (StatescriptGraphVariable v in _graph.Variables)
|
||||
{
|
||||
variableDropdown.AddItem(v.VariableName);
|
||||
}
|
||||
|
||||
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Output, index);
|
||||
var selectedIndex = 0;
|
||||
|
||||
if (binding?.Resolver is VariableResolverResource varRes
|
||||
&& !string.IsNullOrEmpty(varRes.VariableName))
|
||||
{
|
||||
for (var i = 0; i < _graph.Variables.Count; i++)
|
||||
{
|
||||
if (_graph.Variables[i].VariableName == varRes.VariableName)
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_graph.Variables.Count > 0)
|
||||
{
|
||||
variableDropdown.Selected = selectedIndex;
|
||||
|
||||
if (binding is null)
|
||||
{
|
||||
var variableName = _graph.Variables[selectedIndex].VariableName;
|
||||
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver =
|
||||
new VariableResolverResource { VariableName = variableName };
|
||||
}
|
||||
}
|
||||
|
||||
var capturedIndex = index;
|
||||
variableDropdown.ItemSelected += selectedItem => OnOutputVariableDropdownItemSelected(selectedItem, capturedIndex);
|
||||
|
||||
hBox.AddChild(variableDropdown);
|
||||
}
|
||||
|
||||
private void OnOutputVariableDropdownItemSelected(long x, int index)
|
||||
{
|
||||
if (NodeResource is null || _graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
var variableName = _graph.Variables[(int)x].VariableName;
|
||||
var newResolver = new VariableResolverResource { VariableName = variableName };
|
||||
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver = newResolver;
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Change Output Variable", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)StatescriptPropertyDirection.Output,
|
||||
index,
|
||||
(StatescriptResolverResource)newResolver.Duplicate());
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)StatescriptPropertyDirection.Output,
|
||||
index,
|
||||
oldResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
PropertyBindingChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void ShowResolverEditorUI(
|
||||
Func<NodeEditorProperty> factory,
|
||||
StatescriptNodeProperty? existingBinding,
|
||||
Type expectedType,
|
||||
VBoxContainer container,
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex,
|
||||
bool isArray = false)
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NodeEditorProperty resolverEditor = factory();
|
||||
|
||||
var key = new PropertySlotKey(direction, propertyIndex);
|
||||
|
||||
resolverEditor.Setup(
|
||||
_graph,
|
||||
existingBinding,
|
||||
expectedType,
|
||||
() => SaveResolverEditorWithUndo(resolverEditor, direction, propertyIndex),
|
||||
isArray);
|
||||
|
||||
resolverEditor.LayoutSizeChanged += ResetSize;
|
||||
|
||||
container.AddChild(resolverEditor);
|
||||
|
||||
_activeResolverEditors[key] = resolverEditor;
|
||||
}
|
||||
|
||||
private void SaveResolverEditorWithUndo(
|
||||
NodeEditorProperty resolverEditor,
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StatescriptNodeProperty? existing = FindBinding(direction, propertyIndex);
|
||||
var oldResolver = existing?.Resolver?.Duplicate() as StatescriptResolverResource;
|
||||
|
||||
SaveResolverEditor(resolverEditor, direction, propertyIndex);
|
||||
|
||||
StatescriptNodeProperty? updated = FindBinding(direction, propertyIndex);
|
||||
var newResolver = updated?.Resolver?.Duplicate() as StatescriptResolverResource;
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Change Node Property", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)direction,
|
||||
propertyIndex,
|
||||
newResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)direction,
|
||||
propertyIndex,
|
||||
oldResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
PropertyBindingChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void SaveResolverEditor(
|
||||
NodeEditorProperty resolverEditor,
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StatescriptNodeProperty binding = EnsureBinding(direction, propertyIndex);
|
||||
resolverEditor.SaveTo(binding);
|
||||
}
|
||||
|
||||
private sealed class InputPropertyContext(
|
||||
List<Func<NodeEditorProperty>> resolverFactories,
|
||||
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
|
||||
VBoxContainer editorContainer)
|
||||
{
|
||||
public List<Func<NodeEditorProperty>> ResolverFactories { get; } = resolverFactories;
|
||||
|
||||
public StatescriptNodeDiscovery.InputPropertyInfo PropInfo { get; } = propInfo;
|
||||
|
||||
public VBoxContainer EditorContainer { get; } = editorContainer;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8iw3e8i3f0w8
|
||||
628
addons/forge/editor/statescript/StatescriptGraphNode.cs
Normal file
628
addons/forge/editor/statescript/StatescriptGraphNode.cs
Normal file
@@ -0,0 +1,628 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Visual GraphNode representation for a single Statescript node in the editor.
|
||||
/// Supports both built-in node types (Entry/Exit) and dynamically discovered concrete types.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
public partial class StatescriptGraphNode : GraphNode, ISerializationListener
|
||||
{
|
||||
private const string FoldInputKey = "_fold_input";
|
||||
private const string FoldOutputKey = "_fold_output";
|
||||
private const string CustomWidthKey = "_custom_width";
|
||||
|
||||
private static readonly Color _entryColor = new(0x2a4a8dff);
|
||||
private static readonly Color _exitColor = new(0x8a549aff);
|
||||
private static readonly Color _actionColor = new(0x3a7856ff);
|
||||
private static readonly Color _conditionColor = new(0x99811fff);
|
||||
private static readonly Color _stateColor = new(0xa52c38ff);
|
||||
private static readonly Color _eventColor = new(0xabb2bfff);
|
||||
private static readonly Color _subgraphColor = new(0xc678ddff);
|
||||
private static readonly Color _inputPropertyColor = new(0x61afefff);
|
||||
private static readonly Color _outputVariableColor = new(0xe5c07bff);
|
||||
private static readonly Color _highlightColor = new(0x56b6c2ff);
|
||||
|
||||
private readonly Dictionary<PropertySlotKey, NodeEditorProperty> _activeResolverEditors = [];
|
||||
private readonly Dictionary<FoldableContainer, string> _foldableKeys = [];
|
||||
|
||||
private StatescriptNodeDiscovery.NodeTypeInfo? _typeInfo;
|
||||
private StatescriptGraph? _graph;
|
||||
private EditorUndoRedoManager? _undoRedo;
|
||||
private CustomNodeEditor? _activeCustomEditor;
|
||||
private bool _resizeConnected;
|
||||
private float _widthBeforeResize;
|
||||
private string? _highlightedVariableName;
|
||||
private bool _isHighlighted;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a property binding has been modified in the UI.
|
||||
/// </summary>
|
||||
public event Action? PropertyBindingChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying node resource.
|
||||
/// </summary>
|
||||
public StatescriptNode? NodeResource { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
||||
/// </summary>
|
||||
/// <param name="undoRedo">The undo/redo manager from the editor plugin.</param>
|
||||
public void SetUndoRedo(EditorUndoRedoManager? undoRedo)
|
||||
{
|
||||
_undoRedo = undoRedo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
||||
/// </summary>
|
||||
/// <returns>The undo/redo manager, or null if not set.</returns>
|
||||
public EditorUndoRedoManager? GetUndoRedo()
|
||||
{
|
||||
return _undoRedo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the highlight state based on the given variable name.
|
||||
/// </summary>
|
||||
/// <param name="variableName">The variable name to highlight, or null to clear.</param>
|
||||
public void SetHighlightedVariable(string? variableName)
|
||||
{
|
||||
_highlightedVariableName = variableName;
|
||||
_isHighlighted = !string.IsNullOrEmpty(variableName) && ReferencesVariable(variableName!);
|
||||
ApplyHighlightBorder();
|
||||
UpdateChildHighlights();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this visual node from a resource, optionally within the context of a graph.
|
||||
/// </summary>
|
||||
/// <param name="resource">The node resource to display.</param>
|
||||
/// <param name="graph">The owning graph resource (needed for variable dropdowns).</param>
|
||||
public void Initialize(StatescriptNode resource, StatescriptGraph? graph = null)
|
||||
{
|
||||
NodeResource = resource;
|
||||
_graph = graph;
|
||||
_activeResolverEditors.Clear();
|
||||
_foldableKeys.Clear();
|
||||
|
||||
Name = resource.NodeId;
|
||||
Title = resource.Title;
|
||||
PositionOffset = resource.PositionOffset;
|
||||
CustomMinimumSize = new Vector2(240, 0);
|
||||
Resizable = true;
|
||||
|
||||
RestoreCustomWidth();
|
||||
|
||||
if (!_resizeConnected)
|
||||
{
|
||||
_widthBeforeResize = CustomMinimumSize.X;
|
||||
ResizeRequest += OnResizeRequest;
|
||||
ResizeEnd += OnResizeEnd;
|
||||
_resizeConnected = true;
|
||||
}
|
||||
|
||||
ClearSlots();
|
||||
|
||||
if (resource.NodeType is StatescriptNodeType.Entry or StatescriptNodeType.Exit
|
||||
|| string.IsNullOrEmpty(resource.RuntimeTypeName))
|
||||
{
|
||||
SetupNodeByType(resource.NodeType);
|
||||
ApplyBottomPadding();
|
||||
return;
|
||||
}
|
||||
|
||||
_typeInfo = StatescriptNodeDiscovery.FindByRuntimeTypeName(resource.RuntimeTypeName);
|
||||
if (_typeInfo is not null)
|
||||
{
|
||||
SetupFromTypeInfo(_typeInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetupNodeByType(resource.NodeType);
|
||||
}
|
||||
|
||||
ApplyBottomPadding();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
_inputPropertyContexts.Clear();
|
||||
_foldableKeys.Clear();
|
||||
|
||||
_activeCustomEditor?.Unbind();
|
||||
_activeCustomEditor = null;
|
||||
|
||||
foreach (KeyValuePair<PropertySlotKey, NodeEditorProperty> kvp in
|
||||
_activeResolverEditors.Where(kvp => IsInstanceValid(kvp.Value)))
|
||||
{
|
||||
kvp.Value.ClearCallbacks();
|
||||
}
|
||||
|
||||
_activeResolverEditors.Clear();
|
||||
PropertyBindingChanged = null;
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
|
||||
internal FoldableContainer AddPropertySectionDividerInternal(
|
||||
string sectionTitle,
|
||||
Color color,
|
||||
string foldKey,
|
||||
bool folded)
|
||||
{
|
||||
return AddPropertySectionDivider(sectionTitle, color, foldKey, folded);
|
||||
}
|
||||
|
||||
internal void AddInputPropertyRowInternal(
|
||||
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
|
||||
int index,
|
||||
Control container)
|
||||
{
|
||||
AddInputPropertyRow(propInfo, index, container);
|
||||
}
|
||||
|
||||
internal void AddOutputVariableRowInternal(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
FoldableContainer container)
|
||||
{
|
||||
AddOutputVariableRow(varInfo, index, container);
|
||||
}
|
||||
|
||||
internal bool GetFoldStateInternal(string key)
|
||||
{
|
||||
return GetFoldState(key);
|
||||
}
|
||||
|
||||
internal StatescriptNodeProperty? FindBindingInternal(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
return FindBinding(direction, propertyIndex);
|
||||
}
|
||||
|
||||
internal StatescriptNodeProperty EnsureBindingInternal(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
return EnsureBinding(direction, propertyIndex);
|
||||
}
|
||||
|
||||
internal void RemoveBindingInternal(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
RemoveBinding(direction, propertyIndex);
|
||||
}
|
||||
|
||||
internal void RecordResolverBindingChangeInternal(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex,
|
||||
StatescriptResolverResource? oldResolver,
|
||||
StatescriptResolverResource? newResolver,
|
||||
string actionName)
|
||||
{
|
||||
if (_undoRedo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_undoRedo.CreateAction(actionName, customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)direction,
|
||||
propertyIndex,
|
||||
newResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyResolverBinding,
|
||||
(int)direction,
|
||||
propertyIndex,
|
||||
oldResolver ?? new StatescriptResolverResource());
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
internal void ShowResolverEditorUIInternal(
|
||||
Func<NodeEditorProperty> factory,
|
||||
StatescriptNodeProperty? existingBinding,
|
||||
Type expectedType,
|
||||
VBoxContainer container,
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex,
|
||||
bool isArray = false)
|
||||
{
|
||||
ShowResolverEditorUI(factory, existingBinding, expectedType, container, direction, propertyIndex, isArray);
|
||||
}
|
||||
|
||||
internal void RaisePropertyBindingChangedInternal()
|
||||
{
|
||||
PropertyBindingChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static string GetResolverTypeId(StatescriptResolverResource resolver)
|
||||
{
|
||||
return resolver.ResolverTypeId;
|
||||
}
|
||||
|
||||
private static void ClearContainer(Control container)
|
||||
{
|
||||
foreach (Node child in container.GetChildren())
|
||||
{
|
||||
container.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupFromTypeInfo(StatescriptNodeDiscovery.NodeTypeInfo typeInfo)
|
||||
{
|
||||
var maxSlots = Math.Max(typeInfo.InputPortLabels.Length, typeInfo.OutputPortLabels.Length);
|
||||
|
||||
for (var slot = 0; slot < maxSlots; slot++)
|
||||
{
|
||||
var hBox = new HBoxContainer();
|
||||
hBox.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(hBox);
|
||||
|
||||
if (slot < typeInfo.InputPortLabels.Length)
|
||||
{
|
||||
var inputLabel = new Label
|
||||
{
|
||||
Text = typeInfo.InputPortLabels[slot],
|
||||
};
|
||||
|
||||
hBox.AddChild(inputLabel);
|
||||
SetSlotEnabledLeft(slot, true);
|
||||
SetSlotColorLeft(slot, _eventColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var spacer = new Control();
|
||||
hBox.AddChild(spacer);
|
||||
}
|
||||
|
||||
if (slot < typeInfo.OutputPortLabels.Length)
|
||||
{
|
||||
var outputLabel = new Label
|
||||
{
|
||||
Text = typeInfo.OutputPortLabels[slot],
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
hBox.AddChild(outputLabel);
|
||||
SetSlotEnabledRight(slot, true);
|
||||
Color portColor = typeInfo.IsSubgraphPort[slot] ? _subgraphColor : _eventColor;
|
||||
SetSlotColorRight(slot, portColor);
|
||||
}
|
||||
}
|
||||
|
||||
if (CustomNodeEditorRegistry.TryCreate(typeInfo.RuntimeTypeName, out CustomNodeEditor? customEditor))
|
||||
{
|
||||
Debug.Assert(_graph is not null, "Graph context is required for custom node editors.");
|
||||
Debug.Assert(NodeResource is not null, "Node resource is required for custom node editors.");
|
||||
|
||||
_activeCustomEditor = customEditor;
|
||||
customEditor.Bind(this, _graph, NodeResource, _activeResolverEditors);
|
||||
customEditor.BuildPropertySections(typeInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
_activeCustomEditor = null;
|
||||
BuildDefaultPropertySections(typeInfo);
|
||||
}
|
||||
|
||||
Color titleColor = typeInfo.NodeType switch
|
||||
{
|
||||
StatescriptNodeType.Action => _actionColor,
|
||||
StatescriptNodeType.Condition => _conditionColor,
|
||||
StatescriptNodeType.State => _stateColor,
|
||||
StatescriptNodeType.Entry => _entryColor,
|
||||
StatescriptNodeType.Exit => _exitColor,
|
||||
_ => _entryColor,
|
||||
};
|
||||
|
||||
ApplyTitleBarColor(titleColor);
|
||||
}
|
||||
|
||||
private void BuildDefaultPropertySections(StatescriptNodeDiscovery.NodeTypeInfo typeInfo)
|
||||
{
|
||||
if (typeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
var folded = GetFoldState(FoldInputKey);
|
||||
FoldableContainer inputContainer = AddPropertySectionDivider(
|
||||
"Input Properties",
|
||||
_inputPropertyColor,
|
||||
FoldInputKey,
|
||||
folded);
|
||||
|
||||
for (var i = 0; i < typeInfo.InputPropertiesInfo.Length; i++)
|
||||
{
|
||||
AddInputPropertyRow(typeInfo.InputPropertiesInfo[i], i, inputContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeInfo.OutputVariablesInfo.Length > 0)
|
||||
{
|
||||
var folded = GetFoldState(FoldOutputKey);
|
||||
FoldableContainer outputContainer = AddPropertySectionDivider(
|
||||
"Output Variables",
|
||||
_outputVariableColor,
|
||||
FoldOutputKey,
|
||||
folded);
|
||||
|
||||
for (var i = 0; i < typeInfo.OutputVariablesInfo.Length; i++)
|
||||
{
|
||||
AddOutputVariableRow(typeInfo.OutputVariablesInfo[i], i, outputContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FoldableContainer AddPropertySectionDivider(
|
||||
string sectionTitle,
|
||||
Color color,
|
||||
string foldKey,
|
||||
bool folded)
|
||||
{
|
||||
var divider = new HSeparator { CustomMinimumSize = new Vector2(0, 4) };
|
||||
AddChild(divider);
|
||||
|
||||
var sectionContainer = new FoldableContainer
|
||||
{
|
||||
Title = sectionTitle,
|
||||
Folded = folded,
|
||||
};
|
||||
|
||||
sectionContainer.AddThemeColorOverride("font_color", color);
|
||||
|
||||
_foldableKeys[sectionContainer] = foldKey;
|
||||
sectionContainer.FoldingChanged += OnSectionFoldingChanged;
|
||||
|
||||
AddChild(sectionContainer);
|
||||
|
||||
return sectionContainer;
|
||||
}
|
||||
|
||||
private void OnSectionFoldingChanged(bool isFolded)
|
||||
{
|
||||
foreach (KeyValuePair<FoldableContainer, string> kvp in _foldableKeys.Where(kvp => IsInstanceValid(kvp.Key)))
|
||||
{
|
||||
var stored = GetFoldState(kvp.Value);
|
||||
if (kvp.Key.Folded != stored)
|
||||
{
|
||||
SetFoldStateWithUndo(kvp.Value, kvp.Key.Folded);
|
||||
}
|
||||
}
|
||||
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private bool GetFoldState(string key)
|
||||
{
|
||||
if (NodeResource is not null && NodeResource.CustomData.TryGetValue(key, out Variant value))
|
||||
{
|
||||
return value.AsBool();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetFoldState(string key, bool folded)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NodeResource.CustomData[key] = Variant.From(folded);
|
||||
}
|
||||
|
||||
private void SetFoldStateWithUndo(string key, bool folded)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldFolded = GetFoldState(key);
|
||||
|
||||
if (oldFolded == folded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetFoldState(key, folded);
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Toggle Fold", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyFoldState,
|
||||
key,
|
||||
folded);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyFoldState,
|
||||
key,
|
||||
oldFolded);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFoldState(string key, bool folded)
|
||||
{
|
||||
SetFoldState(key, folded);
|
||||
RebuildNode();
|
||||
}
|
||||
|
||||
private void OnResizeRequest(Vector2 newMinSize)
|
||||
{
|
||||
CustomMinimumSize = new Vector2(newMinSize.X, 0);
|
||||
Size = new Vector2(newMinSize.X, 0);
|
||||
SaveCustomWidth(newMinSize.X);
|
||||
}
|
||||
|
||||
private void OnResizeEnd(Vector2 newSize)
|
||||
{
|
||||
var newWidth = CustomMinimumSize.X;
|
||||
|
||||
if (_undoRedo is not null && NodeResource is not null
|
||||
&& !Mathf.IsEqualApprox(_widthBeforeResize, newWidth))
|
||||
{
|
||||
var oldWidth = _widthBeforeResize;
|
||||
|
||||
_undoRedo.CreateAction("Resize Node", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyCustomWidth,
|
||||
newWidth);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyCustomWidth,
|
||||
oldWidth);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
|
||||
_widthBeforeResize = newWidth;
|
||||
}
|
||||
|
||||
private void ApplyCustomWidth(float width)
|
||||
{
|
||||
CustomMinimumSize = new Vector2(width, 0);
|
||||
Size = new Vector2(width, 0);
|
||||
SaveCustomWidth(width);
|
||||
}
|
||||
|
||||
private void RestoreCustomWidth()
|
||||
{
|
||||
if (NodeResource is not null
|
||||
&& NodeResource.CustomData.TryGetValue(CustomWidthKey, out Variant value))
|
||||
{
|
||||
var width = (float)value.AsDouble();
|
||||
|
||||
if (width > 0)
|
||||
{
|
||||
CustomMinimumSize = new Vector2(width, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCustomWidth(float width)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NodeResource.CustomData[CustomWidthKey] = Variant.From(width);
|
||||
}
|
||||
|
||||
private void ApplyResolverBinding(
|
||||
int directionInt,
|
||||
int propertyIndex,
|
||||
StatescriptResolverResource resolver)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var direction = (StatescriptPropertyDirection)directionInt;
|
||||
StatescriptNodeProperty binding = EnsureBinding(direction, propertyIndex);
|
||||
binding.Resolver = resolver;
|
||||
RebuildNode();
|
||||
}
|
||||
|
||||
private void RebuildNode()
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EditorUndoRedoManager? savedUndoRedo = _undoRedo;
|
||||
Initialize(NodeResource, _graph);
|
||||
_undoRedo = savedUndoRedo;
|
||||
Size = new Vector2(Size.X, 0);
|
||||
}
|
||||
|
||||
private StatescriptNodeProperty? FindBinding(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (StatescriptNodeProperty binding in NodeResource.PropertyBindings)
|
||||
{
|
||||
if (binding.Direction == direction && binding.PropertyIndex == propertyIndex)
|
||||
{
|
||||
return binding;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private StatescriptNodeProperty EnsureBinding(
|
||||
StatescriptPropertyDirection direction,
|
||||
int propertyIndex)
|
||||
{
|
||||
StatescriptNodeProperty? binding = FindBinding(direction, propertyIndex);
|
||||
|
||||
if (binding is null)
|
||||
{
|
||||
binding = new StatescriptNodeProperty
|
||||
{
|
||||
Direction = direction,
|
||||
PropertyIndex = propertyIndex,
|
||||
};
|
||||
|
||||
NodeResource!.PropertyBindings.Add(binding);
|
||||
}
|
||||
|
||||
return binding;
|
||||
}
|
||||
|
||||
private void RemoveBinding(StatescriptPropertyDirection direction, int propertyIndex)
|
||||
{
|
||||
if (NodeResource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = NodeResource.PropertyBindings.Count - 1; i >= 0; i--)
|
||||
{
|
||||
StatescriptNodeProperty binding = NodeResource.PropertyBindings[i];
|
||||
|
||||
if (binding.Direction == direction && binding.PropertyIndex == propertyIndex)
|
||||
{
|
||||
NodeResource.PropertyBindings.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a property binding slot by direction and index.
|
||||
/// </summary>
|
||||
/// <param name="Direction">The direction of the property (input or output).</param>
|
||||
/// <param name="PropertyIndex">The index of the property within its direction.</param>
|
||||
internal readonly record struct PropertySlotKey(StatescriptPropertyDirection Direction, int PropertyIndex);
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgb5kncrbsgb4
|
||||
540
addons/forge/editor/statescript/StatescriptNodeDiscovery.cs
Normal file
540
addons/forge/editor/statescript/StatescriptNodeDiscovery.cs
Normal file
@@ -0,0 +1,540 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Gamesmiths.Forge.Statescript.Nodes;
|
||||
using Gamesmiths.Forge.Statescript.Ports;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers concrete Statescript node types from loaded assemblies using reflection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Provides port layout information for the editor without requiring node instantiation.
|
||||
/// </remarks>
|
||||
internal static class StatescriptNodeDiscovery
|
||||
{
|
||||
private static List<NodeTypeInfo>? _cachedNodeTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all discovered concrete node types. Results are cached after first discovery.
|
||||
/// </summary>
|
||||
/// <returns>A read-only list of node type info.</returns>
|
||||
internal static IReadOnlyList<NodeTypeInfo> GetDiscoveredNodeTypes()
|
||||
{
|
||||
_cachedNodeTypes ??= DiscoverNodeTypes();
|
||||
return _cachedNodeTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cached discovery results, forcing re-discovery on next access.
|
||||
/// </summary>
|
||||
internal static void InvalidateCache()
|
||||
{
|
||||
_cachedNodeTypes = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="NodeTypeInfo"/> for the given runtime type name.
|
||||
/// </summary>
|
||||
/// <param name="runtimeTypeName">The full type name stored in the resource.</param>
|
||||
/// <returns>The matching node type info, or null if not found.</returns>
|
||||
internal static NodeTypeInfo? FindByRuntimeTypeName(string runtimeTypeName)
|
||||
{
|
||||
IReadOnlyList<NodeTypeInfo> types = GetDiscoveredNodeTypes();
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
if (types[i].RuntimeTypeName == runtimeTypeName)
|
||||
{
|
||||
return types[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<NodeTypeInfo> DiscoverNodeTypes()
|
||||
{
|
||||
var results = new List<NodeTypeInfo>();
|
||||
|
||||
Type actionNodeType = typeof(ActionNode);
|
||||
Type conditionNodeType = typeof(ConditionNode);
|
||||
Type stateNodeOpenType = typeof(StateNode<>);
|
||||
|
||||
// Scan all loaded assemblies for concrete node types.
|
||||
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(x => x is not null).ToArray()!;
|
||||
}
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
if (type.IsAbstract || type.IsGenericTypeDefinition)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the built-in Entry/Exit nodes — they are handled separately.
|
||||
if (type == typeof(EntryNode) || type == typeof(ExitNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actionNodeType.IsAssignableFrom(type))
|
||||
{
|
||||
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.Action));
|
||||
}
|
||||
else if (conditionNodeType.IsAssignableFrom(type))
|
||||
{
|
||||
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.Condition));
|
||||
}
|
||||
else if (IsConcreteStateNode(type, stateNodeOpenType))
|
||||
{
|
||||
results.Add(BuildNodeTypeInfo(type, StatescriptNodeType.State));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.Sort((a, b) => string.CompareOrdinal(a.DisplayName, b.DisplayName));
|
||||
return results;
|
||||
}
|
||||
|
||||
private static bool IsConcreteStateNode(Type type, Type stateNodeOpenType)
|
||||
{
|
||||
Type? current = type.BaseType;
|
||||
while (current is not null)
|
||||
{
|
||||
if (current.IsGenericType && current.GetGenericTypeDefinition() == stateNodeOpenType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static NodeTypeInfo BuildNodeTypeInfo(Type type, StatescriptNodeType nodeType)
|
||||
{
|
||||
var displayName = FormatDisplayName(type.Name);
|
||||
var runtimeTypeName = type.FullName!;
|
||||
|
||||
// Get constructor parameter names.
|
||||
var constructorParamNames = GetConstructorParameterNames(type);
|
||||
|
||||
// Determine ports and description by instantiating a temporary node.
|
||||
string[] inputLabels;
|
||||
string[] outputLabels;
|
||||
bool[] isSubgraph;
|
||||
string description;
|
||||
InputPropertyInfo[] inputPropertiesInfo;
|
||||
OutputVariableInfo[] outputVariablesInfo;
|
||||
|
||||
try
|
||||
{
|
||||
Node tempNode = CreateTemporaryNode(type);
|
||||
inputLabels = GetInputPortLabels(tempNode, nodeType);
|
||||
outputLabels = GetOutputPortLabels(tempNode, nodeType);
|
||||
isSubgraph = GetSubgraphFlags(tempNode);
|
||||
description = tempNode.Description;
|
||||
inputPropertiesInfo = GetInputPropertiesInfo(tempNode);
|
||||
outputVariablesInfo = GetOutputVariablesInfo(tempNode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to default port layout based on base type.
|
||||
PortLayout[] portLayouts = GetDefaultPortLayout(nodeType);
|
||||
inputLabels = [.. portLayouts.Select(x => x.InputLabel)];
|
||||
outputLabels = [.. portLayouts.Select(x => x.OutputLabel)];
|
||||
isSubgraph = [.. portLayouts.Select(x => x.IsSubgraph)];
|
||||
description = $"{displayName} node.";
|
||||
inputPropertiesInfo = [];
|
||||
outputVariablesInfo = [];
|
||||
}
|
||||
|
||||
return new NodeTypeInfo(
|
||||
displayName,
|
||||
runtimeTypeName,
|
||||
nodeType,
|
||||
inputLabels,
|
||||
outputLabels,
|
||||
isSubgraph,
|
||||
constructorParamNames,
|
||||
description,
|
||||
inputPropertiesInfo,
|
||||
outputVariablesInfo);
|
||||
}
|
||||
|
||||
private static Node CreateTemporaryNode(Type type)
|
||||
{
|
||||
// Try to find the primary constructor or the one with the fewest parameters.
|
||||
ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
if (constructors.Length == 0)
|
||||
{
|
||||
return (Node)Activator.CreateInstance(type)!;
|
||||
}
|
||||
|
||||
// Sort by parameter count, prefer the fewest.
|
||||
ConstructorInfo constructor = constructors.OrderBy(x => x.GetParameters().Length).First();
|
||||
ParameterInfo[] parameters = constructor.GetParameters();
|
||||
|
||||
var args = new object[parameters.Length];
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
Type paramType = parameters[i].ParameterType;
|
||||
|
||||
if (paramType == typeof(Forge.Core.StringKey))
|
||||
{
|
||||
args[i] = new Forge.Core.StringKey("_placeholder_");
|
||||
}
|
||||
else if (paramType == typeof(string))
|
||||
{
|
||||
args[i] = string.Empty;
|
||||
}
|
||||
else if (paramType.IsValueType)
|
||||
{
|
||||
args[i] = Activator.CreateInstance(paramType)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
args[i] = null!;
|
||||
}
|
||||
}
|
||||
|
||||
return (Node)constructor.Invoke(args);
|
||||
}
|
||||
|
||||
private static string[] GetConstructorParameterNames(Type type)
|
||||
{
|
||||
ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
if (constructors.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use the constructor with the most parameters (primary constructor).
|
||||
ConstructorInfo constructor = constructors.OrderByDescending(x => x.GetParameters().Length).First();
|
||||
return [.. constructor.GetParameters().Select(x => x.Name ?? string.Empty)];
|
||||
}
|
||||
|
||||
private static string[] GetInputPortLabels(Node node, StatescriptNodeType nodeType)
|
||||
{
|
||||
var count = node.InputPorts.Length;
|
||||
var labels = new string[count];
|
||||
|
||||
switch (nodeType)
|
||||
{
|
||||
case StatescriptNodeType.Action:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "Execute";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case StatescriptNodeType.Condition:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "Condition";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case StatescriptNodeType.State:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "Begin";
|
||||
}
|
||||
|
||||
if (count >= 2)
|
||||
{
|
||||
labels[1] = "Abort";
|
||||
}
|
||||
|
||||
for (var i = 2; i < count; i++)
|
||||
{
|
||||
labels[i] = $"Input {i}";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
labels[i] = $"Input {i}";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
private static string[] GetOutputPortLabels(Node node, StatescriptNodeType nodeType)
|
||||
{
|
||||
var count = node.OutputPorts.Length;
|
||||
var labels = new string[count];
|
||||
|
||||
switch (nodeType)
|
||||
{
|
||||
case StatescriptNodeType.Action:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "Done";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case StatescriptNodeType.Condition:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "True";
|
||||
}
|
||||
|
||||
if (count >= 2)
|
||||
{
|
||||
labels[1] = "False";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case StatescriptNodeType.State:
|
||||
if (count >= 1)
|
||||
{
|
||||
labels[0] = "OnActivate";
|
||||
}
|
||||
|
||||
if (count >= 2)
|
||||
{
|
||||
labels[1] = "OnDeactivate";
|
||||
}
|
||||
|
||||
if (count >= 3)
|
||||
{
|
||||
labels[2] = "OnAbort";
|
||||
}
|
||||
|
||||
if (count >= 4)
|
||||
{
|
||||
labels[3] = "Subgraph";
|
||||
}
|
||||
|
||||
for (var i = 4; i < count; i++)
|
||||
{
|
||||
labels[i] = $"Event {i}";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
labels[i] = $"Output {i}";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
private static bool[] GetSubgraphFlags(Node node)
|
||||
{
|
||||
var count = node.OutputPorts.Length;
|
||||
var flags = new bool[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
flags[i] = node.OutputPorts[i] is SubgraphPort;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static InputPropertyInfo[] GetInputPropertiesInfo(Node node)
|
||||
{
|
||||
var propertiesInfo = new InputPropertyInfo[node.InputProperties.Length];
|
||||
for (var i = 0; i < node.InputProperties.Length; i++)
|
||||
{
|
||||
propertiesInfo[i] = new InputPropertyInfo(
|
||||
node.InputProperties[i].Label,
|
||||
node.InputProperties[i].ExpectedType);
|
||||
}
|
||||
|
||||
return propertiesInfo;
|
||||
}
|
||||
|
||||
private static OutputVariableInfo[] GetOutputVariablesInfo(Node node)
|
||||
{
|
||||
var variablesInfo = new OutputVariableInfo[node.OutputVariables.Length];
|
||||
for (var i = 0; i < node.OutputVariables.Length; i++)
|
||||
{
|
||||
variablesInfo[i] = new OutputVariableInfo(
|
||||
node.OutputVariables[i].Label,
|
||||
node.OutputVariables[i].ValueType,
|
||||
node.OutputVariables[i].Scope);
|
||||
}
|
||||
|
||||
return variablesInfo;
|
||||
}
|
||||
|
||||
private static PortLayout[] GetDefaultPortLayout(
|
||||
StatescriptNodeType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
StatescriptNodeType.Action => [new PortLayout("Execute", "Done", false)],
|
||||
StatescriptNodeType.Condition => [
|
||||
new PortLayout("Condition", "True", false),
|
||||
new PortLayout(string.Empty, "False", false)],
|
||||
StatescriptNodeType.State => [
|
||||
new PortLayout("Begin", "OnActivate", false),
|
||||
new PortLayout("Abort", "OnDeactivate", false),
|
||||
new PortLayout(string.Empty, "OnAbort", false),
|
||||
new PortLayout(string.Empty, "Subgraph", true)],
|
||||
StatescriptNodeType.Entry => throw new NotImplementedException(),
|
||||
StatescriptNodeType.Exit => throw new NotImplementedException(),
|
||||
_ => [new PortLayout("Input", "Output", false)],
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDisplayName(string typeName)
|
||||
{
|
||||
// Remove common suffixes.
|
||||
if (typeName.EndsWith("Node", StringComparison.Ordinal))
|
||||
{
|
||||
typeName = typeName[..^4];
|
||||
}
|
||||
|
||||
// Insert spaces before capital letters for camelCase names.
|
||||
var result = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < typeName.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(typeName[i]) && !char.IsUpper(typeName[i - 1]))
|
||||
{
|
||||
result.Append(' ');
|
||||
}
|
||||
|
||||
result.Append(typeName[i]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a discovered concrete node type and its port layout.
|
||||
/// </summary>
|
||||
internal sealed class NodeTypeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display name for this node type (e.g., "Timer", "Set Variable", "Expression").
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR type name used for serialization (typically the type's full name).
|
||||
/// </summary>
|
||||
public string RuntimeTypeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node category (Action, Condition, State).
|
||||
/// </summary>
|
||||
public StatescriptNodeType NodeType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input port labels for this node type.
|
||||
/// </summary>
|
||||
public string[] InputPortLabels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output port labels for this node type.
|
||||
/// </summary>
|
||||
public string[] OutputPortLabels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether each output port is a subgraph port.
|
||||
/// </summary>
|
||||
public bool[] IsSubgraphPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the constructor parameter names for this node type.
|
||||
/// </summary>
|
||||
public string[] ConstructorParameterNames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a brief description for this node type, shown in the Add Node dialog.
|
||||
/// Read from the <see cref="Node.Description"/> property at discovery time.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input property declarations for this node type.
|
||||
/// </summary>
|
||||
public InputPropertyInfo[] InputPropertiesInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output variable declarations for this node type.
|
||||
/// </summary>
|
||||
public OutputVariableInfo[] OutputVariablesInfo { get; }
|
||||
|
||||
public NodeTypeInfo(
|
||||
string displayName,
|
||||
string runtimeTypeName,
|
||||
StatescriptNodeType nodeType,
|
||||
string[] inputPortLabels,
|
||||
string[] outputPortLabels,
|
||||
bool[] isSubgraphPort,
|
||||
string[] constructorParameterNames,
|
||||
string description,
|
||||
InputPropertyInfo[] inputPropertiesInfo,
|
||||
OutputVariableInfo[] outputVariablesInfo)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
RuntimeTypeName = runtimeTypeName;
|
||||
NodeType = nodeType;
|
||||
InputPortLabels = inputPortLabels;
|
||||
OutputPortLabels = outputPortLabels;
|
||||
IsSubgraphPort = isSubgraphPort;
|
||||
ConstructorParameterNames = constructorParameterNames;
|
||||
Description = description;
|
||||
InputPropertiesInfo = inputPropertiesInfo;
|
||||
OutputVariablesInfo = outputVariablesInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an input property declared by a node type.
|
||||
/// </summary>
|
||||
/// <param name="Label">The human-readable label for this input property.</param>
|
||||
/// <param name="ExpectedType">The type the node expects to read.</param>
|
||||
/// <param name="IsArray">Whether the input expects an array of values.</param>
|
||||
internal readonly record struct InputPropertyInfo(string Label, Type ExpectedType, bool IsArray = false);
|
||||
|
||||
/// <summary>
|
||||
/// Describes an output variable declared by a node type.
|
||||
/// </summary>
|
||||
/// <param name="Label">The human-readable label for this output variable.</param>
|
||||
/// <param name="ValueType">The type the node writes.</param>
|
||||
/// <param name="Scope">The default scope for this output variable.</param>
|
||||
internal readonly record struct OutputVariableInfo(string Label, Type ValueType, VariableScope Scope);
|
||||
|
||||
private record struct PortLayout(string InputLabel, string OutputLabel, bool IsSubgraph);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cb2mf4xojoxal
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of available <see cref="NodeEditorProperty"/> implementations. Resolver editors are discovered
|
||||
/// automatically via reflection. Any concrete subclass of <see cref="NodeEditorProperty"/> in the executing assembly is
|
||||
/// registered and becomes available in node input property dropdowns.
|
||||
/// </summary>
|
||||
internal static class StatescriptResolverRegistry
|
||||
{
|
||||
private static readonly List<Func<NodeEditorProperty>> _factories = [];
|
||||
|
||||
static StatescriptResolverRegistry()
|
||||
{
|
||||
Type[] allTypes = Assembly.GetExecutingAssembly().GetTypes();
|
||||
|
||||
foreach (Type type in allTypes.Where(
|
||||
x => x.IsSubclassOf(typeof(NodeEditorProperty)) && !x.IsAbstract))
|
||||
{
|
||||
Type captured = type;
|
||||
_factories.Add(() => (NodeEditorProperty)Activator.CreateInstance(captured)!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets factory functions for all resolver editors compatible with the given expected type.
|
||||
/// </summary>
|
||||
/// <param name="expectedType">The type expected by the node input property.</param>
|
||||
/// <returns>A list of compatible resolver editor factories.</returns>
|
||||
public static List<Func<NodeEditorProperty>> GetCompatibleFactories(Type expectedType)
|
||||
{
|
||||
var result = new List<Func<NodeEditorProperty>>();
|
||||
|
||||
foreach (Func<NodeEditorProperty> factory in _factories)
|
||||
{
|
||||
using NodeEditorProperty temp = factory();
|
||||
|
||||
if (temp.IsCompatibleWith(expectedType))
|
||||
{
|
||||
result.Add(factory);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bq3g4cbysmedf
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
internal sealed partial class StatescriptVariablePanel
|
||||
{
|
||||
private static void SetArrayElementValue(StatescriptGraphVariable variable, int index, Variant newValue)
|
||||
{
|
||||
variable.InitialArrayValues[index] = newValue;
|
||||
variable.EmitChanged();
|
||||
}
|
||||
|
||||
private void SetVariableValue(StatescriptGraphVariable variable, Variant newValue)
|
||||
{
|
||||
Variant oldValue = variable.InitialValue;
|
||||
|
||||
variable.InitialValue = newValue;
|
||||
variable.EmitChanged();
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction(
|
||||
$"Change Variable '{variable.VariableName}'",
|
||||
customContext: _graph);
|
||||
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.ApplyVariableValue,
|
||||
variable,
|
||||
newValue);
|
||||
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.ApplyVariableValue,
|
||||
variable,
|
||||
oldValue);
|
||||
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVariableValue(StatescriptGraphVariable variable, Variant value)
|
||||
{
|
||||
variable.InitialValue = value;
|
||||
variable.EmitChanged();
|
||||
RebuildList();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
|
||||
private void DoAddVariable(StatescriptGraph graph, StatescriptGraphVariable variable)
|
||||
{
|
||||
graph.Variables.Add(variable);
|
||||
RebuildList();
|
||||
VariablesChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void UndoAddVariable(StatescriptGraph graph, StatescriptGraphVariable variable)
|
||||
{
|
||||
graph.Variables.Remove(variable);
|
||||
RebuildList();
|
||||
VariablesChanged?.Invoke();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
|
||||
private void DoRemoveVariable(StatescriptGraph graph, StatescriptGraphVariable variable, int index)
|
||||
{
|
||||
graph.Variables.RemoveAt(index);
|
||||
ClearReferencesToVariable(variable.VariableName);
|
||||
RebuildList();
|
||||
VariablesChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void UndoRemoveVariable(StatescriptGraph graph, StatescriptGraphVariable variable, int index)
|
||||
{
|
||||
if (index >= graph.Variables.Count)
|
||||
{
|
||||
graph.Variables.Add(variable);
|
||||
}
|
||||
else
|
||||
{
|
||||
graph.Variables.Insert(index, variable);
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
VariablesChanged?.Invoke();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
|
||||
private void DoAddArrayElement(StatescriptGraphVariable variable, Variant value)
|
||||
{
|
||||
variable.InitialArrayValues.Add(value);
|
||||
variable.EmitChanged();
|
||||
_expandedArrays.Add(variable.VariableName);
|
||||
SaveExpandedArrayState();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoAddArrayElement(StatescriptGraphVariable variable)
|
||||
{
|
||||
if (variable.InitialArrayValues.Count > 0)
|
||||
{
|
||||
variable.InitialArrayValues.RemoveAt(variable.InitialArrayValues.Count - 1);
|
||||
variable.EmitChanged();
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
|
||||
private void DoRemoveArrayElement(StatescriptGraphVariable variable, int index)
|
||||
{
|
||||
variable.InitialArrayValues.RemoveAt(index);
|
||||
variable.EmitChanged();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
private void UndoRemoveArrayElement(StatescriptGraphVariable variable, int index, Variant value)
|
||||
{
|
||||
if (index >= variable.InitialArrayValues.Count)
|
||||
{
|
||||
variable.InitialArrayValues.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
variable.InitialArrayValues.Insert(index, value);
|
||||
}
|
||||
|
||||
variable.EmitChanged();
|
||||
RebuildList();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
|
||||
private void DoSetArrayExpanded(string variableName, bool expanded)
|
||||
{
|
||||
if (expanded)
|
||||
{
|
||||
_expandedArrays.Add(variableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_expandedArrays.Remove(variableName);
|
||||
}
|
||||
|
||||
SaveExpandedArrayState();
|
||||
RebuildList();
|
||||
VariableUndoRedoPerformed?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbrse4fxsk87x
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
internal sealed partial class StatescriptVariablePanel
|
||||
{
|
||||
private Control CreateScalarValueEditor(StatescriptGraphVariable variable)
|
||||
{
|
||||
if (variable.VariableType == StatescriptVariableType.Bool)
|
||||
{
|
||||
var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
hBox.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
variable.InitialValue.AsBool(),
|
||||
x => SetVariableValue(variable, Variant.From(x))));
|
||||
|
||||
return hBox;
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsIntegerType(variable.VariableType)
|
||||
|| StatescriptEditorControls.IsFloatType(variable.VariableType))
|
||||
{
|
||||
var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
variable.VariableType,
|
||||
variable.InitialValue.AsDouble(),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.IsIntegerType(variable.VariableType)
|
||||
? Variant.From((long)x)
|
||||
: Variant.From(x);
|
||||
SetVariableValue(variable, newValue);
|
||||
});
|
||||
|
||||
hBox.AddChild(spin);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(variable.VariableType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateVectorEditor(
|
||||
variable.VariableType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(
|
||||
variable.InitialValue,
|
||||
variable.VariableType,
|
||||
x),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.BuildVectorVariant(
|
||||
variable.VariableType,
|
||||
x);
|
||||
SetVariableValue(variable, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
var fallback = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
fallback.AddChild(new Label { Text = variable.VariableType.ToString() });
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private VBoxContainer CreateArrayValueEditor(StatescriptGraphVariable variable)
|
||||
{
|
||||
var vBox = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var headerRow = new HBoxContainer();
|
||||
vBox.AddChild(headerRow);
|
||||
|
||||
var isExpanded = _expandedArrays.Contains(variable.VariableName);
|
||||
|
||||
var elementsContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Visible = isExpanded,
|
||||
};
|
||||
|
||||
var toggleButton = new Button
|
||||
{
|
||||
Text = $"Array (size {variable.InitialArrayValues.Count})",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = isExpanded,
|
||||
};
|
||||
|
||||
toggleButton.Toggled += x =>
|
||||
{
|
||||
elementsContainer.Visible = x;
|
||||
|
||||
var wasExpanded = !x;
|
||||
|
||||
if (x)
|
||||
{
|
||||
_expandedArrays.Add(variable.VariableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_expandedArrays.Remove(variable.VariableName);
|
||||
}
|
||||
|
||||
SaveExpandedArrayState();
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Toggle Array Expand", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoSetArrayExpanded,
|
||||
variable.VariableName,
|
||||
x);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.DoSetArrayExpanded,
|
||||
variable.VariableName,
|
||||
wasExpanded);
|
||||
_undoRedo.CommitAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
headerRow.AddChild(toggleButton);
|
||||
|
||||
var addElementButton = new Button
|
||||
{
|
||||
Icon = _addIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Add Element",
|
||||
CustomMinimumSize = new Vector2(24, 24),
|
||||
};
|
||||
|
||||
addElementButton.Pressed += () =>
|
||||
{
|
||||
Variant defaultValue =
|
||||
StatescriptVariableTypeConverter.CreateDefaultGodotVariant(variable.VariableType);
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Add Array Element", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoAddArrayElement,
|
||||
variable,
|
||||
defaultValue);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.UndoAddArrayElement,
|
||||
variable);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoAddArrayElement(variable, defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
headerRow.AddChild(addElementButton);
|
||||
|
||||
vBox.AddChild(elementsContainer);
|
||||
|
||||
for (var i = 0; i < variable.InitialArrayValues.Count; i++)
|
||||
{
|
||||
var capturedIndex = i;
|
||||
|
||||
if (variable.VariableType == StatescriptVariableType.Bool)
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
variable.InitialArrayValues[i].AsBool(),
|
||||
x => SetArrayElementValue(
|
||||
variable,
|
||||
capturedIndex,
|
||||
Variant.From(x))));
|
||||
|
||||
AddArrayElementRemoveButton(elementRow, variable, capturedIndex);
|
||||
}
|
||||
else if (StatescriptEditorControls.IsVectorType(variable.VariableType))
|
||||
{
|
||||
var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementVBox);
|
||||
|
||||
var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementVBox.AddChild(labelRow);
|
||||
labelRow.AddChild(new Label
|
||||
{
|
||||
Text = $"[{i}]",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
AddArrayElementRemoveButton(labelRow, variable, capturedIndex);
|
||||
|
||||
VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor(
|
||||
variable.VariableType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(
|
||||
variable.InitialArrayValues[capturedIndex],
|
||||
variable.VariableType,
|
||||
x),
|
||||
x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.BuildVectorVariant(
|
||||
variable.VariableType,
|
||||
x);
|
||||
SetArrayElementValue(variable, capturedIndex, newValue);
|
||||
});
|
||||
|
||||
elementVBox.AddChild(vectorEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
EditorSpinSlider elementSpin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
variable.VariableType,
|
||||
variable.InitialArrayValues[i].AsDouble(),
|
||||
onChanged: x =>
|
||||
{
|
||||
Variant newValue = StatescriptEditorControls.IsIntegerType(variable.VariableType)
|
||||
? Variant.From((long)x)
|
||||
: Variant.From(x);
|
||||
SetArrayElementValue(variable, capturedIndex, newValue);
|
||||
});
|
||||
|
||||
elementRow.AddChild(elementSpin);
|
||||
AddArrayElementRemoveButton(elementRow, variable, capturedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private void AddArrayElementRemoveButton(
|
||||
HBoxContainer row,
|
||||
StatescriptGraphVariable variable,
|
||||
int elementIndex)
|
||||
{
|
||||
var removeElementButton = new Button
|
||||
{
|
||||
Icon = _removeIcon,
|
||||
Flat = true,
|
||||
CustomMinimumSize = new Vector2(24, 24),
|
||||
};
|
||||
|
||||
removeElementButton.Pressed += () =>
|
||||
{
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
Variant removedValue = variable.InitialArrayValues[elementIndex];
|
||||
|
||||
_undoRedo.CreateAction("Remove Array Element", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(
|
||||
this,
|
||||
MethodName.DoRemoveArrayElement,
|
||||
variable,
|
||||
elementIndex);
|
||||
_undoRedo.AddUndoMethod(
|
||||
this,
|
||||
MethodName.UndoRemoveArrayElement,
|
||||
variable,
|
||||
elementIndex,
|
||||
removedValue);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoRemoveArrayElement(variable, elementIndex);
|
||||
}
|
||||
};
|
||||
|
||||
row.AddChild(removeElementButton);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bhgni65dto1ul
|
||||
529
addons/forge/editor/statescript/StatescriptVariablePanel.cs
Normal file
529
addons/forge/editor/statescript/StatescriptVariablePanel.cs
Normal file
@@ -0,0 +1,529 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
||||
|
||||
/// <summary>
|
||||
/// Right-side panel for editing graph variables. Variables are created with a name and type via a creation dialog.
|
||||
/// Once created, only the initial value can be edited. To change name or type, delete and recreate the variable.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISerializationListener
|
||||
{
|
||||
private const string ExpandedArraysMetaKey = "_expanded_arrays";
|
||||
|
||||
private static readonly Color _variableColor = new(0xe5c07bff);
|
||||
private static readonly Color _highlightColor = new(0x56b6c2ff);
|
||||
|
||||
private readonly HashSet<string> _expandedArrays = [];
|
||||
|
||||
private StatescriptGraph? _graph;
|
||||
private VBoxContainer? _variableList;
|
||||
private Button? _addButton;
|
||||
|
||||
private Window? _creationDialog;
|
||||
private LineEdit? _newNameEdit;
|
||||
private OptionButton? _newTypeDropdown;
|
||||
private CheckBox? _newArrayToggle;
|
||||
|
||||
private Texture2D? _addIcon;
|
||||
private Texture2D? _removeIcon;
|
||||
|
||||
private EditorUndoRedoManager? _undoRedo;
|
||||
|
||||
private string? _selectedVariableName;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when any variable is added, removed, or its value changes.
|
||||
/// </summary>
|
||||
public event Action? VariablesChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an undo/redo action modifies the variable panel, so the dock can auto-expand it.
|
||||
/// </summary>
|
||||
public event Action? VariableUndoRedoPerformed;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user selects or deselects a variable for highlighting.
|
||||
/// </summary>
|
||||
public event Action<string?>? VariableHighlightChanged;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
_addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
_removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
CustomMinimumSize = new Vector2(360, 0);
|
||||
|
||||
var headerHBox = new HBoxContainer();
|
||||
AddChild(headerHBox);
|
||||
|
||||
var titleLabel = new Label
|
||||
{
|
||||
Text = "Graph Variables",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
headerHBox.AddChild(titleLabel);
|
||||
|
||||
_addButton = new Button
|
||||
{
|
||||
Icon = _addIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Add Variable",
|
||||
CustomMinimumSize = new Vector2(28, 28),
|
||||
};
|
||||
|
||||
_addButton.Pressed += OnAddPressed;
|
||||
headerHBox.AddChild(_addButton);
|
||||
|
||||
var separator = new HSeparator();
|
||||
AddChild(separator);
|
||||
|
||||
var scrollContainer = new ScrollContainer
|
||||
{
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(scrollContainer);
|
||||
|
||||
_variableList = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
scrollContainer.AddChild(_variableList);
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
base._ExitTree();
|
||||
|
||||
if (_addButton is not null)
|
||||
{
|
||||
_addButton.Pressed -= OnAddPressed;
|
||||
}
|
||||
|
||||
_creationDialog?.QueueFree();
|
||||
_creationDialog = null;
|
||||
_newNameEdit = null;
|
||||
_newTypeDropdown = null;
|
||||
_newArrayToggle = null;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
if (_addButton is not null)
|
||||
{
|
||||
_addButton.Pressed -= OnAddPressed;
|
||||
}
|
||||
|
||||
if (_variableList is not null)
|
||||
{
|
||||
foreach (Node child in _variableList.GetChildren())
|
||||
{
|
||||
_variableList.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
_creationDialog?.Free();
|
||||
_creationDialog = null;
|
||||
_newNameEdit = null;
|
||||
_newTypeDropdown = null;
|
||||
_newArrayToggle = null;
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
if (_addButton is not null)
|
||||
{
|
||||
_addButton.Pressed += OnAddPressed;
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the graph to display variables for.
|
||||
/// </summary>
|
||||
/// <param name="graph">The graph resource, or null to clear.</param>
|
||||
public void SetGraph(StatescriptGraph? graph)
|
||||
{
|
||||
_graph = graph;
|
||||
LoadExpandedArrayState();
|
||||
RebuildList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
||||
/// </summary>
|
||||
/// <param name="undoRedo">The undo/redo manager from the editor plugin.</param>
|
||||
public void SetUndoRedo(EditorUndoRedoManager undoRedo)
|
||||
{
|
||||
_undoRedo = undoRedo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the variable list UI from the current graph.
|
||||
/// </summary>
|
||||
public void RebuildList()
|
||||
{
|
||||
if (_variableList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in _variableList.GetChildren())
|
||||
{
|
||||
_variableList.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
|
||||
if (_graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _graph.Variables.Count; i++)
|
||||
{
|
||||
AddVariableRow(_graph.Variables[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveExpandedArrayState()
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var packed = new string[_expandedArrays.Count];
|
||||
_expandedArrays.CopyTo(packed);
|
||||
_graph.SetMeta(ExpandedArraysMetaKey, Variant.From(packed));
|
||||
}
|
||||
|
||||
private void LoadExpandedArrayState()
|
||||
{
|
||||
_expandedArrays.Clear();
|
||||
|
||||
if (_graph?.HasMeta(ExpandedArraysMetaKey) != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Variant meta = _graph.GetMeta(ExpandedArraysMetaKey);
|
||||
|
||||
if (meta.VariantType == Variant.Type.PackedStringArray)
|
||||
{
|
||||
foreach (var name in meta.AsStringArray())
|
||||
{
|
||||
_expandedArrays.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVariableRow(StatescriptGraphVariable variable, int index)
|
||||
{
|
||||
if (_variableList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rowContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_variableList.AddChild(rowContainer);
|
||||
|
||||
var headerRow = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
rowContainer.AddChild(headerRow);
|
||||
|
||||
var isSelected = _selectedVariableName == variable.VariableName;
|
||||
|
||||
var nameButton = new Button
|
||||
{
|
||||
Text = variable.VariableName,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Flat = true,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = isSelected,
|
||||
Alignment = HorizontalAlignment.Left,
|
||||
};
|
||||
|
||||
Color buttonColor = isSelected ? _highlightColor : _variableColor;
|
||||
nameButton.AddThemeColorOverride("font_color", buttonColor);
|
||||
nameButton.AddThemeColorOverride("font_pressed_color", _highlightColor);
|
||||
nameButton.AddThemeColorOverride("font_hover_color", buttonColor.Lightened(0.2f));
|
||||
nameButton.AddThemeColorOverride("font_hover_pressed_color", _highlightColor.Lightened(0.2f));
|
||||
nameButton.AddThemeFontOverride(
|
||||
"font",
|
||||
EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts"));
|
||||
|
||||
nameButton.Toggled += pressed =>
|
||||
{
|
||||
if (pressed)
|
||||
{
|
||||
_selectedVariableName = variable.VariableName;
|
||||
}
|
||||
else if (_selectedVariableName == variable.VariableName)
|
||||
{
|
||||
_selectedVariableName = null;
|
||||
}
|
||||
|
||||
RebuildList();
|
||||
VariableHighlightChanged?.Invoke(_selectedVariableName);
|
||||
};
|
||||
|
||||
headerRow.AddChild(nameButton);
|
||||
|
||||
var typeLabel = new Label
|
||||
{
|
||||
Text = $"({StatescriptVariableTypeConverter.GetDisplayName(variable.VariableType)}"
|
||||
+ (variable.IsArray ? "[])" : ")"),
|
||||
};
|
||||
|
||||
typeLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f));
|
||||
headerRow.AddChild(typeLabel);
|
||||
|
||||
var capturedIndex = index;
|
||||
|
||||
var deleteButton = new Button
|
||||
{
|
||||
Icon = _removeIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Remove Variable",
|
||||
CustomMinimumSize = new Vector2(28, 28),
|
||||
};
|
||||
|
||||
deleteButton.Pressed += () => OnDeletePressed(capturedIndex);
|
||||
headerRow.AddChild(deleteButton);
|
||||
|
||||
if (!variable.IsArray)
|
||||
{
|
||||
Control valueEditor = CreateScalarValueEditor(variable);
|
||||
rowContainer.AddChild(valueEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
VBoxContainer arrayEditor = CreateArrayValueEditor(variable);
|
||||
rowContainer.AddChild(arrayEditor);
|
||||
}
|
||||
|
||||
rowContainer.AddChild(new HSeparator());
|
||||
}
|
||||
|
||||
private void OnAddPressed()
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ShowCreationDialog();
|
||||
}
|
||||
|
||||
private void ShowCreationDialog()
|
||||
{
|
||||
_creationDialog?.QueueFree();
|
||||
|
||||
_creationDialog = new AcceptDialog
|
||||
{
|
||||
Title = "Add Variable",
|
||||
Size = new Vector2I(300, 160),
|
||||
Exclusive = true,
|
||||
};
|
||||
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
var nameRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(nameRow);
|
||||
|
||||
nameRow.AddChild(new Label { Text = "Name:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newNameEdit = new LineEdit
|
||||
{
|
||||
Text = GenerateUniqueName(),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
nameRow.AddChild(_newNameEdit);
|
||||
|
||||
var typeRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(typeRow);
|
||||
|
||||
typeRow.AddChild(new Label { Text = "Type:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newTypeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
StatescriptVariableType[] allTypes = StatescriptVariableTypeConverter.GetAllTypes();
|
||||
|
||||
for (var t = 0; t < allTypes.Length; t++)
|
||||
{
|
||||
_newTypeDropdown.AddItem(StatescriptVariableTypeConverter.GetDisplayName(allTypes[t]), t);
|
||||
}
|
||||
|
||||
_newTypeDropdown.Selected = (int)StatescriptVariableType.Int;
|
||||
typeRow.AddChild(_newTypeDropdown);
|
||||
|
||||
var arrayRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(arrayRow);
|
||||
|
||||
arrayRow.AddChild(new Label { Text = "Array:", CustomMinimumSize = new Vector2(60, 0) });
|
||||
|
||||
_newArrayToggle = new CheckBox();
|
||||
arrayRow.AddChild(_newArrayToggle);
|
||||
|
||||
_creationDialog.AddChild(vBox);
|
||||
|
||||
((AcceptDialog)_creationDialog).Confirmed += OnCreationConfirmed;
|
||||
|
||||
AddChild(_creationDialog);
|
||||
_creationDialog.PopupCentered();
|
||||
}
|
||||
|
||||
private void OnCreationConfirmed()
|
||||
{
|
||||
if (_graph is null || _newNameEdit is null || _newTypeDropdown is null || _newArrayToggle is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var name = _newNameEdit.Text.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(name) || HasVariableNamed(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedIndex = _newTypeDropdown.Selected;
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedId = _newTypeDropdown.GetItemId(selectedIndex);
|
||||
var varType = (StatescriptVariableType)selectedId;
|
||||
|
||||
var newVariable = new StatescriptGraphVariable
|
||||
{
|
||||
VariableName = name,
|
||||
VariableType = varType,
|
||||
IsArray = _newArrayToggle.ButtonPressed,
|
||||
InitialValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(varType),
|
||||
};
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Add Graph Variable", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoAddVariable, _graph!, newVariable);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, _graph!, newVariable);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoAddVariable(_graph, newVariable);
|
||||
}
|
||||
|
||||
_creationDialog?.QueueFree();
|
||||
_creationDialog = null;
|
||||
_newNameEdit = null;
|
||||
_newTypeDropdown = null;
|
||||
_newArrayToggle = null;
|
||||
}
|
||||
|
||||
private void OnDeletePressed(int index)
|
||||
{
|
||||
if (_graph is null || index < 0 || index >= _graph.Variables.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StatescriptGraphVariable variable = _graph.Variables[index];
|
||||
|
||||
if (_undoRedo is not null)
|
||||
{
|
||||
_undoRedo.CreateAction("Remove Graph Variable", customContext: _graph);
|
||||
_undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, _graph!, variable, index);
|
||||
_undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, _graph!, variable, index);
|
||||
_undoRedo.CommitAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
DoRemoveVariable(_graph, variable, index);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearReferencesToVariable(string variableName)
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (StatescriptNode node in _graph.Nodes)
|
||||
{
|
||||
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
|
||||
{
|
||||
if (binding.Resolver is VariableResolverResource varRes
|
||||
&& varRes.VariableName == variableName)
|
||||
{
|
||||
varRes.VariableName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateUniqueName()
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return "variable";
|
||||
}
|
||||
|
||||
const string baseName = "variable";
|
||||
var counter = 1;
|
||||
var name = baseName;
|
||||
|
||||
while (HasVariableNamed(name))
|
||||
{
|
||||
name = $"{baseName}_{counter++}";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private bool HasVariableNamed(string name)
|
||||
{
|
||||
if (_graph is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (StatescriptGraphVariable variable in _graph.Variables)
|
||||
{
|
||||
if (variable.VariableName == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dax3ghnqv8jet
|
||||
@@ -0,0 +1,825 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.NodeEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Custom node editor for the <c>SetVariableNode</c>. Dynamically filters the Input (value resolver) based on the
|
||||
/// selected target variable's type. Supports both Graph and Shared variable scopes.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class SetVariableNodeEditor : CustomNodeEditor
|
||||
{
|
||||
private const string FoldInputKey = "_fold_input";
|
||||
private const string FoldOutputKey = "_fold_output";
|
||||
private const string ScopeKey = "_output_scope";
|
||||
|
||||
private readonly List<string> _setPaths = [];
|
||||
private readonly List<string> _variableNames = [];
|
||||
|
||||
private StatescriptVariableType? _resolvedType;
|
||||
private bool _resolvedIsArray;
|
||||
|
||||
private StatescriptNodeDiscovery.NodeTypeInfo? _cachedTypeInfo;
|
||||
private VBoxContainer? _cachedInputEditorContainer;
|
||||
private VBoxContainer? _cachedTargetContainer;
|
||||
private int _cachedOutputIndex;
|
||||
|
||||
private bool _isSharedScope;
|
||||
|
||||
private OptionButton? _setDropdown;
|
||||
private OptionButton? _sharedVarDropdown;
|
||||
private string _selectedSetPath = string.Empty;
|
||||
private string _selectedSharedVarName = string.Empty;
|
||||
private StatescriptVariableType _selectedSharedVarType = StatescriptVariableType.Int;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HandledRuntimeTypeName => "Gamesmiths.Forge.Statescript.Nodes.Action.SetVariableNode";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void BuildPropertySections(StatescriptNodeDiscovery.NodeTypeInfo typeInfo)
|
||||
{
|
||||
_cachedTypeInfo = typeInfo;
|
||||
|
||||
var inputFolded = GetFoldState(FoldInputKey);
|
||||
FoldableContainer inputContainer = AddPropertySectionDivider(
|
||||
"Input Properties",
|
||||
InputPropertyColor,
|
||||
FoldInputKey,
|
||||
inputFolded);
|
||||
|
||||
var inputEditorContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_cachedInputEditorContainer = inputEditorContainer;
|
||||
|
||||
inputContainer.AddChild(inputEditorContainer);
|
||||
|
||||
var outputFolded = GetFoldState(FoldOutputKey);
|
||||
FoldableContainer outputContainer = AddPropertySectionDivider(
|
||||
"Output Variables",
|
||||
OutputVariableColor,
|
||||
FoldOutputKey,
|
||||
outputFolded);
|
||||
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
|
||||
StatescriptNodeProperty? outputBinding = FindBinding(StatescriptPropertyDirection.Output, 0);
|
||||
_isSharedScope = outputBinding?.Resolver is SharedVariableResolverResource;
|
||||
|
||||
if (outputBinding is null
|
||||
&& NodeResource.CustomData.TryGetValue(ScopeKey, out Variant scopeValue))
|
||||
{
|
||||
_isSharedScope = scopeValue.AsInt32() == (int)VariableScope.Shared;
|
||||
}
|
||||
|
||||
ResolveTypeFromBinding(outputBinding);
|
||||
|
||||
if (typeInfo.OutputVariablesInfo.Length > 0)
|
||||
{
|
||||
AddTargetVariableRow(
|
||||
typeInfo.OutputVariablesInfo[0],
|
||||
0,
|
||||
outputContainer);
|
||||
}
|
||||
|
||||
if (typeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
RebuildInputUI(typeInfo.InputPropertiesInfo[0], inputEditorContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override void Unbind()
|
||||
{
|
||||
base.Unbind();
|
||||
_cachedTypeInfo = null;
|
||||
_cachedInputEditorContainer = null;
|
||||
_cachedTargetContainer = null;
|
||||
_setDropdown = null;
|
||||
_sharedVarDropdown = null;
|
||||
}
|
||||
|
||||
private static List<string> FindAllSharedVariableSetPaths()
|
||||
{
|
||||
var results = new List<string>();
|
||||
EditorFileSystemDirectory root = EditorInterface.Singleton.GetResourceFilesystem().GetFilesystem();
|
||||
ScanFilesystemDirectory(root, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void ScanFilesystemDirectory(EditorFileSystemDirectory dir, List<string> results)
|
||||
{
|
||||
for (var i = 0; i < dir.GetFileCount(); i++)
|
||||
{
|
||||
var path = dir.GetFilePath(i);
|
||||
|
||||
if (!path.EndsWith(".tres", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& !path.EndsWith(".res", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Resource resource = ResourceLoader.Load(path);
|
||||
|
||||
if (resource is ForgeSharedVariableSet)
|
||||
{
|
||||
results.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < dir.GetSubdirCount(); i++)
|
||||
{
|
||||
ScanFilesystemDirectory(dir.GetSubdir(i), results);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveTypeFromBinding(StatescriptNodeProperty? outputBinding)
|
||||
{
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
|
||||
if (outputBinding?.Resolver is VariableResolverResource varRes
|
||||
&& !string.IsNullOrEmpty(varRes.VariableName))
|
||||
{
|
||||
foreach (StatescriptGraphVariable v in Graph.Variables)
|
||||
{
|
||||
if (v.VariableName == varRes.VariableName)
|
||||
{
|
||||
_resolvedType = v.VariableType;
|
||||
_resolvedIsArray = v.IsArray;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outputBinding?.Resolver is SharedVariableResolverResource sharedRes
|
||||
&& !string.IsNullOrEmpty(sharedRes.VariableName))
|
||||
{
|
||||
_selectedSetPath = sharedRes.SharedVariableSetPath;
|
||||
_selectedSharedVarName = sharedRes.VariableName;
|
||||
_selectedSharedVarType = sharedRes.VariableType;
|
||||
_resolvedType = sharedRes.VariableType;
|
||||
_resolvedIsArray = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTargetVariableRow(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
FoldableContainer sectionContainer)
|
||||
{
|
||||
_cachedOutputIndex = index;
|
||||
|
||||
var outerVBox = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
sectionContainer.AddChild(outerVBox);
|
||||
|
||||
// Scope toggle row.
|
||||
var scopeRow = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
outerVBox.AddChild(scopeRow);
|
||||
|
||||
var scopeLabel = new Label
|
||||
{
|
||||
Text = "Scope",
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
};
|
||||
|
||||
scopeLabel.AddThemeColorOverride("font_color", OutputVariableColor);
|
||||
scopeRow.AddChild(scopeLabel);
|
||||
|
||||
var graphButton = new CheckBox
|
||||
{
|
||||
Text = "Graph",
|
||||
ButtonPressed = !_isSharedScope,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var sharedButton = new CheckBox
|
||||
{
|
||||
Text = "Shared",
|
||||
ButtonPressed = _isSharedScope,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
var buttonGroup = new ButtonGroup();
|
||||
graphButton.ButtonGroup = buttonGroup;
|
||||
sharedButton.ButtonGroup = buttonGroup;
|
||||
|
||||
scopeRow.AddChild(graphButton);
|
||||
scopeRow.AddChild(sharedButton);
|
||||
|
||||
// Target variable container (rebuilt when scope changes).
|
||||
var targetContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_cachedTargetContainer = targetContainer;
|
||||
outerVBox.AddChild(targetContainer);
|
||||
|
||||
RebuildTargetUI(varInfo, index, targetContainer);
|
||||
|
||||
graphButton.Pressed += () => OnScopeChanged(false, varInfo, index);
|
||||
sharedButton.Pressed += () => OnScopeChanged(true, varInfo, index);
|
||||
}
|
||||
|
||||
private void OnScopeChanged(
|
||||
bool isShared,
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index)
|
||||
{
|
||||
if (_isSharedScope == isShared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var oldInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
_isSharedScope = isShared;
|
||||
NodeResource.CustomData[ScopeKey] = Variant.From(isShared ? (int)VariableScope.Shared : (int)VariableScope.Graph);
|
||||
|
||||
// Clear the output binding since scope changed.
|
||||
RemoveBinding(StatescriptPropertyDirection.Output, index);
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
|
||||
// Reset shared variable state when switching away.
|
||||
if (!isShared)
|
||||
{
|
||||
_selectedSetPath = string.Empty;
|
||||
_selectedSharedVarName = string.Empty;
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
if (_cachedTargetContainer is not null)
|
||||
{
|
||||
ClearContainer(_cachedTargetContainer);
|
||||
RebuildTargetUI(varInfo, index, _cachedTargetContainer);
|
||||
}
|
||||
|
||||
// Clear and rebuild input since type changed.
|
||||
RemoveBinding(StatescriptPropertyDirection.Input, 0);
|
||||
ActiveResolverEditors.Remove(new PropertySlotKey(StatescriptPropertyDirection.Input, 0));
|
||||
|
||||
if (_cachedTypeInfo is not null
|
||||
&& _cachedInputEditorContainer is not null
|
||||
&& _cachedTypeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
RebuildInputUI(_cachedTypeInfo.InputPropertiesInfo[0], _cachedInputEditorContainer);
|
||||
}
|
||||
|
||||
var newResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var newInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Output,
|
||||
index,
|
||||
oldResolver,
|
||||
newResolver,
|
||||
"Change Variable Scope");
|
||||
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Input,
|
||||
0,
|
||||
oldInputResolver,
|
||||
newInputResolver,
|
||||
"Change Variable Scope Input");
|
||||
|
||||
RaisePropertyBindingChanged();
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private void RebuildTargetUI(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
VBoxContainer container)
|
||||
{
|
||||
if (_isSharedScope)
|
||||
{
|
||||
BuildSharedVariableUI(varInfo, container);
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildGraphVariableUI(varInfo, index, container);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildGraphVariableUI(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
int index,
|
||||
VBoxContainer container)
|
||||
{
|
||||
var hBox = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
container.AddChild(hBox);
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = varInfo.Label,
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
};
|
||||
|
||||
nameLabel.AddThemeColorOverride("font_color", OutputVariableColor);
|
||||
hBox.AddChild(nameLabel);
|
||||
|
||||
var dropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
dropdown.SetMeta("is_variable_dropdown", true);
|
||||
|
||||
dropdown.AddItem("(None)");
|
||||
|
||||
foreach (StatescriptGraphVariable v in Graph.Variables)
|
||||
{
|
||||
dropdown.AddItem(v.VariableName);
|
||||
}
|
||||
|
||||
StatescriptNodeProperty? binding = FindBinding(StatescriptPropertyDirection.Output, index);
|
||||
var selectedIndex = 0;
|
||||
|
||||
if (binding?.Resolver is VariableResolverResource varRes
|
||||
&& !string.IsNullOrEmpty(varRes.VariableName))
|
||||
{
|
||||
for (var i = 0; i < Graph.Variables.Count; i++)
|
||||
{
|
||||
if (Graph.Variables[i].VariableName == varRes.VariableName)
|
||||
{
|
||||
selectedIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropdown.Selected = selectedIndex;
|
||||
|
||||
if (selectedIndex == 0)
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Output, index);
|
||||
}
|
||||
|
||||
dropdown.ItemSelected += OnTargetVariableDropdownItemSelected;
|
||||
|
||||
hBox.AddChild(dropdown);
|
||||
}
|
||||
|
||||
private void BuildSharedVariableUI(
|
||||
StatescriptNodeDiscovery.OutputVariableInfo varInfo,
|
||||
VBoxContainer container)
|
||||
{
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = varInfo.Label,
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
};
|
||||
|
||||
nameLabel.AddThemeColorOverride("font_color", OutputVariableColor);
|
||||
container.AddChild(nameLabel);
|
||||
|
||||
// Set dropdown row.
|
||||
var setRow = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
container.AddChild(setRow);
|
||||
|
||||
setRow.AddChild(new Label
|
||||
{
|
||||
Text = "Set:",
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_setDropdown = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
PopulateSetDropdown();
|
||||
setRow.AddChild(_setDropdown);
|
||||
|
||||
// Variable dropdown row.
|
||||
var varRow = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
container.AddChild(varRow);
|
||||
|
||||
varRow.AddChild(new Label
|
||||
{
|
||||
Text = "Var:",
|
||||
CustomMinimumSize = new Vector2(60, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_sharedVarDropdown = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
|
||||
PopulateSharedVariableDropdown();
|
||||
varRow.AddChild(_sharedVarDropdown);
|
||||
|
||||
_setDropdown.ItemSelected += OnSharedSetDropdownItemSelected;
|
||||
_sharedVarDropdown.ItemSelected += OnSharedVariableDropdownItemSelected;
|
||||
}
|
||||
|
||||
private void PopulateSetDropdown()
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setDropdown.Clear();
|
||||
_setPaths.Clear();
|
||||
|
||||
_setDropdown.AddItem("(None)");
|
||||
_setPaths.Add(string.Empty);
|
||||
|
||||
foreach (var path in FindAllSharedVariableSetPaths())
|
||||
{
|
||||
var displayName = path[(path.LastIndexOf('/') + 1)..];
|
||||
|
||||
if (displayName.EndsWith(".tres", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
displayName = displayName[..^5];
|
||||
}
|
||||
|
||||
_setDropdown.AddItem(displayName);
|
||||
_setPaths.Add(path);
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _setPaths.Count; i++)
|
||||
{
|
||||
if (_setPaths[i] == _selectedSetPath)
|
||||
{
|
||||
_setDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_setDropdown.Selected = 0;
|
||||
_selectedSetPath = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateSharedVariableDropdown()
|
||||
{
|
||||
if (_sharedVarDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sharedVarDropdown.Clear();
|
||||
_variableNames.Clear();
|
||||
|
||||
_sharedVarDropdown.AddItem("(None)");
|
||||
_variableNames.Add(string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedSetPath) && ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is not null)
|
||||
{
|
||||
foreach (var variableName in set.Variables.Select(x => x.VariableName))
|
||||
{
|
||||
if (string.IsNullOrEmpty(variableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_sharedVarDropdown.AddItem(variableName);
|
||||
_variableNames.Add(variableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _variableNames.Count; i++)
|
||||
{
|
||||
if (_variableNames[i] == _selectedSharedVarName)
|
||||
{
|
||||
_sharedVarDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_sharedVarDropdown.Selected = 0;
|
||||
_selectedSharedVarName = string.Empty;
|
||||
}
|
||||
|
||||
private void OnSharedSetDropdownItemSelected(long x)
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _setDropdown.Selected;
|
||||
|
||||
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var oldInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
_selectedSetPath = idx >= 0 && idx < _setPaths.Count ? _setPaths[idx] : string.Empty;
|
||||
_selectedSharedVarName = string.Empty;
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
|
||||
PopulateSharedVariableDropdown();
|
||||
UpdateSharedOutputBinding();
|
||||
|
||||
var newResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
StatescriptVariableType? previousType = _resolvedType;
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
|
||||
if (previousType != _resolvedType)
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Input, 0);
|
||||
ActiveResolverEditors.Remove(new PropertySlotKey(StatescriptPropertyDirection.Input, 0));
|
||||
}
|
||||
|
||||
if (_cachedTypeInfo is not null
|
||||
&& _cachedInputEditorContainer is not null
|
||||
&& _cachedTypeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
RebuildInputUI(_cachedTypeInfo.InputPropertiesInfo[0], _cachedInputEditorContainer);
|
||||
}
|
||||
|
||||
var newInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Output,
|
||||
_cachedOutputIndex,
|
||||
oldResolver,
|
||||
newResolver,
|
||||
"Change Shared Variable Set");
|
||||
|
||||
if (previousType != _resolvedType)
|
||||
{
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Input,
|
||||
0,
|
||||
oldInputResolver,
|
||||
newInputResolver,
|
||||
"Change Shared Variable Set Input");
|
||||
}
|
||||
|
||||
RaisePropertyBindingChanged();
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private void OnSharedVariableDropdownItemSelected(long x)
|
||||
{
|
||||
if (_sharedVarDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _sharedVarDropdown.Selected;
|
||||
|
||||
var oldResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var oldInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
StatescriptVariableType? previousType = _resolvedType;
|
||||
var previousIsArray = _resolvedIsArray;
|
||||
|
||||
if (idx >= 0 && idx < _variableNames.Count)
|
||||
{
|
||||
_selectedSharedVarName = _variableNames[idx];
|
||||
ResolveSharedVariableType();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedSharedVarName = string.Empty;
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
UpdateSharedOutputBinding();
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedSharedVarName))
|
||||
{
|
||||
_resolvedType = _selectedSharedVarType;
|
||||
_resolvedIsArray = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
}
|
||||
|
||||
if (previousType != _resolvedType || previousIsArray != _resolvedIsArray)
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Input, 0);
|
||||
ActiveResolverEditors.Remove(new PropertySlotKey(StatescriptPropertyDirection.Input, 0));
|
||||
}
|
||||
|
||||
if (_cachedTypeInfo is not null
|
||||
&& _cachedInputEditorContainer is not null
|
||||
&& _cachedTypeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
RebuildInputUI(_cachedTypeInfo.InputPropertiesInfo[0], _cachedInputEditorContainer);
|
||||
}
|
||||
|
||||
var newResolver = FindBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var newInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Output,
|
||||
_cachedOutputIndex,
|
||||
oldResolver,
|
||||
newResolver,
|
||||
"Change Shared Target Variable");
|
||||
|
||||
if (previousType != _resolvedType || previousIsArray != _resolvedIsArray)
|
||||
{
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Input,
|
||||
0,
|
||||
oldInputResolver,
|
||||
newInputResolver,
|
||||
"Change Shared Target Variable Input");
|
||||
}
|
||||
|
||||
RaisePropertyBindingChanged();
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private void UpdateSharedOutputBinding()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedSharedVarName) || string.IsNullOrEmpty(_selectedSetPath))
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureBinding(StatescriptPropertyDirection.Output, _cachedOutputIndex).Resolver =
|
||||
new SharedVariableResolverResource
|
||||
{
|
||||
SharedVariableSetPath = _selectedSetPath,
|
||||
VariableName = _selectedSharedVarName,
|
||||
VariableType = _selectedSharedVarType,
|
||||
};
|
||||
}
|
||||
|
||||
private void ResolveSharedVariableType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedSetPath)
|
||||
|| string.IsNullOrEmpty(_selectedSharedVarName)
|
||||
|| !ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is null)
|
||||
{
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ForgeSharedVariableDefinition def in set.Variables)
|
||||
{
|
||||
if (def.VariableName == _selectedSharedVarName)
|
||||
{
|
||||
_selectedSharedVarType = def.VariableType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedSharedVarType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
private void OnTargetVariableDropdownItemSelected(long x)
|
||||
{
|
||||
if (_cachedTypeInfo is null || _cachedInputEditorContainer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _cachedOutputIndex;
|
||||
var variableIndex = (int)x - 1;
|
||||
|
||||
StatescriptVariableType? previousType = _resolvedType;
|
||||
var previousIsArray = _resolvedIsArray;
|
||||
|
||||
var oldOutputResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var oldInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
if (variableIndex < 0)
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Output, index);
|
||||
_resolvedType = null;
|
||||
_resolvedIsArray = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var variableName = Graph.Variables[variableIndex].VariableName;
|
||||
EnsureBinding(StatescriptPropertyDirection.Output, index).Resolver =
|
||||
new VariableResolverResource { VariableName = variableName };
|
||||
|
||||
_resolvedType = Graph.Variables[variableIndex].VariableType;
|
||||
_resolvedIsArray = Graph.Variables[variableIndex].IsArray;
|
||||
}
|
||||
|
||||
if (previousType != _resolvedType || previousIsArray != _resolvedIsArray)
|
||||
{
|
||||
RemoveBinding(StatescriptPropertyDirection.Input, 0);
|
||||
|
||||
var inputKey = new PropertySlotKey(StatescriptPropertyDirection.Input, 0);
|
||||
|
||||
ActiveResolverEditors.Remove(inputKey);
|
||||
}
|
||||
|
||||
if (_cachedTypeInfo.InputPropertiesInfo.Length > 0)
|
||||
{
|
||||
RebuildInputUI(_cachedTypeInfo.InputPropertiesInfo[0], _cachedInputEditorContainer);
|
||||
}
|
||||
|
||||
var newOutputResolver = FindBinding(StatescriptPropertyDirection.Output, index)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
var newInputResolver = FindBinding(StatescriptPropertyDirection.Input, 0)?.Resolver?.Duplicate()
|
||||
as StatescriptResolverResource;
|
||||
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Output,
|
||||
index,
|
||||
oldOutputResolver,
|
||||
newOutputResolver,
|
||||
"Change Target Variable");
|
||||
|
||||
if (previousType != _resolvedType || previousIsArray != _resolvedIsArray)
|
||||
{
|
||||
RecordResolverBindingChange(
|
||||
StatescriptPropertyDirection.Input,
|
||||
0,
|
||||
oldInputResolver,
|
||||
newInputResolver,
|
||||
"Change Target Variable Input");
|
||||
}
|
||||
|
||||
RaisePropertyBindingChanged();
|
||||
ResetSize();
|
||||
}
|
||||
|
||||
private void RebuildInputUI(
|
||||
StatescriptNodeDiscovery.InputPropertyInfo propInfo,
|
||||
VBoxContainer container)
|
||||
{
|
||||
ClearContainer(container);
|
||||
|
||||
if (_resolvedType is null)
|
||||
{
|
||||
var placeholder = new Label
|
||||
{
|
||||
Text = "Select target variable first",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
placeholder.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.4f));
|
||||
container.AddChild(placeholder);
|
||||
ResetSize();
|
||||
return;
|
||||
}
|
||||
|
||||
Type resolvedClrType = StatescriptVariableTypeConverter.ToSystemType(_resolvedType.Value);
|
||||
|
||||
AddInputPropertyRow(
|
||||
new StatescriptNodeDiscovery.InputPropertyInfo(propInfo.Label, resolvedClrType, _resolvedIsArray),
|
||||
0,
|
||||
container);
|
||||
|
||||
ResetSize();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://us4bxyl7143x
|
||||
@@ -0,0 +1,366 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds a node input property to an activation data field. Uses a two-step selection: first
|
||||
/// select the <see cref="IActivationDataProvider"/> implementation, then select a compatible field from that provider.
|
||||
/// Providers are discovered via reflection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A graph supports only one activation data provider. Once any other node in the graph references a provider, the
|
||||
/// provider dropdown is locked to that provider. The user only needs to clear the bindings on other nodes to unlock
|
||||
/// the dropdown.
|
||||
/// </remarks>
|
||||
[Tool]
|
||||
internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _providerClassNames = [];
|
||||
private readonly List<string> _fieldNames = [];
|
||||
|
||||
private StatescriptGraph? _graph;
|
||||
private StatescriptNodeProperty? _currentProperty;
|
||||
|
||||
private OptionButton? _providerDropdown;
|
||||
private OptionButton? _fieldDropdown;
|
||||
private Action? _onChanged;
|
||||
private Type _expectedType = typeof(Variant128);
|
||||
|
||||
private string _selectedProviderClassName = string.Empty;
|
||||
private string _selectedFieldName = string.Empty;
|
||||
private StatescriptVariableType _selectedFieldType = StatescriptVariableType.Int;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Activation Data";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "ActivationData";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
_expectedType = expectedType;
|
||||
_graph = graph;
|
||||
_currentProperty = property;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is ActivationDataResolverResource activationRes)
|
||||
{
|
||||
_selectedProviderClassName = activationRes.ProviderClassName;
|
||||
_selectedFieldName = activationRes.FieldName;
|
||||
_selectedFieldType = activationRes.FieldType;
|
||||
}
|
||||
|
||||
var providerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(providerRow);
|
||||
|
||||
providerRow.AddChild(new Label
|
||||
{
|
||||
Text = "Provider:",
|
||||
CustomMinimumSize = new Vector2(75, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_providerDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateProviderDropdown();
|
||||
providerRow.AddChild(_providerDropdown);
|
||||
|
||||
// Re-scan the graph each time the dropdown opens to pick up changes from other editors.
|
||||
_providerDropdown.GetPopup().AboutToPopup += PopulateProviderDropdown;
|
||||
|
||||
var fieldRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(fieldRow);
|
||||
|
||||
fieldRow.AddChild(new Label
|
||||
{
|
||||
Text = "Field:",
|
||||
CustomMinimumSize = new Vector2(75, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_fieldDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateFieldDropdown();
|
||||
fieldRow.AddChild(_fieldDropdown);
|
||||
|
||||
_providerDropdown.ItemSelected += OnProviderDropdownItemSelected;
|
||||
_fieldDropdown.ItemSelected += OnFieldDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new ActivationDataResolverResource
|
||||
{
|
||||
ProviderClassName = _selectedProviderClassName,
|
||||
FieldName = _selectedFieldName,
|
||||
FieldType = _selectedFieldType,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private static string FindExistingProvider(StatescriptGraph graph, StatescriptNodeProperty? currentProperty)
|
||||
{
|
||||
foreach (StatescriptNode node in graph.Nodes)
|
||||
{
|
||||
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
|
||||
{
|
||||
// Skip the property we're currently editing — the user should be free to change it.
|
||||
if (ReferenceEquals(binding, currentProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (binding.Resolver is ActivationDataResolverResource { ProviderClassName.Length: > 0 } resolver)
|
||||
{
|
||||
return resolver.ProviderClassName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static IActivationDataProvider? InstantiateProvider(string className)
|
||||
{
|
||||
if (string.IsNullOrEmpty(className))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type? type = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(
|
||||
x => typeof(IActivationDataProvider).IsAssignableFrom(x)
|
||||
&& !x.IsAbstract
|
||||
&& !x.IsInterface
|
||||
&& x.Name == className);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Activator.CreateInstance(type) as IActivationDataProvider;
|
||||
}
|
||||
|
||||
private void OnProviderDropdownItemSelected(long index)
|
||||
{
|
||||
if (_providerDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _providerDropdown.Selected;
|
||||
_selectedProviderClassName = idx >= 0 && idx < _providerClassNames.Count
|
||||
? _providerClassNames[idx]
|
||||
: string.Empty;
|
||||
_selectedFieldName = string.Empty;
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
|
||||
PopulateFieldDropdown();
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnFieldDropdownItemSelected(long index)
|
||||
{
|
||||
if (_fieldDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dropdownIndex = _fieldDropdown.Selected;
|
||||
|
||||
if (dropdownIndex >= 0 && dropdownIndex < _fieldNames.Count)
|
||||
{
|
||||
_selectedFieldName = _fieldNames[dropdownIndex];
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
ResolveFieldType();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedFieldName = string.Empty;
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateProviderDropdown()
|
||||
{
|
||||
if (_providerDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_providerDropdown.Clear();
|
||||
_providerClassNames.Clear();
|
||||
|
||||
// Always add a (None) option to allow deselecting.
|
||||
_providerDropdown.AddItem("(None)");
|
||||
_providerClassNames.Add(string.Empty);
|
||||
|
||||
// Re-scan the graph each time to pick up changes from other editors.
|
||||
var graphLockedProvider = _graph is not null
|
||||
? FindExistingProvider(_graph, _currentProperty)
|
||||
: string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(graphLockedProvider))
|
||||
{
|
||||
// Another node already uses a provider: only show that one (plus None).
|
||||
_providerDropdown.AddItem(graphLockedProvider);
|
||||
_providerClassNames.Add(graphLockedProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var name in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(x => typeof(IActivationDataProvider).IsAssignableFrom(x)
|
||||
&& !x.IsAbstract
|
||||
&& !x.IsInterface)
|
||||
.Select(x => x.Name))
|
||||
{
|
||||
_providerDropdown.AddItem(name);
|
||||
_providerClassNames.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedProviderClassName))
|
||||
{
|
||||
for (var i = 0; i < _providerClassNames.Count; i++)
|
||||
{
|
||||
if (_providerClassNames[i] == _selectedProviderClassName)
|
||||
{
|
||||
_providerDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to (None).
|
||||
_providerDropdown.Selected = 0;
|
||||
_selectedProviderClassName = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateFieldDropdown()
|
||||
{
|
||||
if (_fieldDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fieldDropdown.Clear();
|
||||
_fieldNames.Clear();
|
||||
|
||||
// Always add a (None) option.
|
||||
_fieldDropdown.AddItem("(None)");
|
||||
_fieldNames.Add(string.Empty);
|
||||
|
||||
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
|
||||
|
||||
if (provider is not null)
|
||||
{
|
||||
foreach (ForgeActivationDataField field in provider.GetFields())
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.FieldName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_expectedType != typeof(Variant128)
|
||||
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, field.FieldType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_fieldDropdown.AddItem(field.FieldName);
|
||||
_fieldNames.Add(field.FieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
for (var i = 0; i < _fieldNames.Count; i++)
|
||||
{
|
||||
if (_fieldNames[i] == _selectedFieldName)
|
||||
{
|
||||
_fieldDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to (None).
|
||||
_fieldDropdown.Selected = 0;
|
||||
_selectedFieldName = string.Empty;
|
||||
}
|
||||
|
||||
private void ResolveFieldType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedProviderClassName) || string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ForgeActivationDataField field in provider.GetFields())
|
||||
{
|
||||
if (field.FieldName == _selectedFieldName)
|
||||
{
|
||||
_selectedFieldType = field.FieldType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvegkmbda17em
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that reads a value from a Forge entity attribute. Shows attribute set and attribute dropdowns.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class AttributeResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private OptionButton? _setDropdown;
|
||||
private OptionButton? _attributeDropdown;
|
||||
private string _selectedSetClass = string.Empty;
|
||||
private string _selectedAttribute = string.Empty;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Attribute";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Attribute";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(int) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is AttributeResolverResource attrRes)
|
||||
{
|
||||
_selectedSetClass = attrRes.AttributeSetClass;
|
||||
_selectedAttribute = attrRes.AttributeName;
|
||||
}
|
||||
|
||||
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(setRow);
|
||||
|
||||
setRow.AddChild(new Label
|
||||
{
|
||||
Text = "Set:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateSetDropdown();
|
||||
setRow.AddChild(_setDropdown);
|
||||
|
||||
var attrRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(attrRow);
|
||||
|
||||
attrRow.AddChild(new Label
|
||||
{
|
||||
Text = "Attr:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_attributeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateAttributeDropdown();
|
||||
attrRow.AddChild(_attributeDropdown);
|
||||
|
||||
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
|
||||
_attributeDropdown.ItemSelected += OnAttributeDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new AttributeResolverResource
|
||||
{
|
||||
AttributeSetClass = _selectedSetClass,
|
||||
AttributeName = _selectedAttribute,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnSetDropdownItemSelected(long index)
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSetClass = _setDropdown.GetItemText(_setDropdown.Selected);
|
||||
_selectedAttribute = string.Empty;
|
||||
PopulateAttributeDropdown();
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnAttributeDropdownItemSelected(long index)
|
||||
{
|
||||
if (_attributeDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedAttribute = _attributeDropdown.GetItemText(_attributeDropdown.Selected);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateSetDropdown()
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setDropdown.Clear();
|
||||
|
||||
foreach (var option in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
_setDropdown.AddItem(option);
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedSetClass))
|
||||
{
|
||||
for (var i = 0; i < _setDropdown.GetItemCount(); i++)
|
||||
{
|
||||
if (_setDropdown.GetItemText(i) == _selectedSetClass)
|
||||
{
|
||||
_setDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first if available.
|
||||
if (_setDropdown.GetItemCount() > 0)
|
||||
{
|
||||
_setDropdown.Selected = 0;
|
||||
_selectedSetClass = _setDropdown.GetItemText(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateAttributeDropdown()
|
||||
{
|
||||
if (_attributeDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_attributeDropdown.Clear();
|
||||
|
||||
foreach (var option in EditorUtils.GetAttributeOptions(_selectedSetClass))
|
||||
{
|
||||
_attributeDropdown.AddItem(option);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedAttribute))
|
||||
{
|
||||
for (var i = 0; i < _attributeDropdown.GetItemCount(); i++)
|
||||
{
|
||||
if (_attributeDropdown.GetItemText(i) == _selectedAttribute)
|
||||
{
|
||||
_attributeDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_attributeDropdown.GetItemCount() > 0)
|
||||
{
|
||||
_attributeDropdown.Selected = 0;
|
||||
_selectedAttribute = _attributeDropdown.GetItemText(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://ciagvn5l8gnbq
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript.Properties;
|
||||
using Godot;
|
||||
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that compares two nested numeric resolvers and produces a boolean result. Supports nesting any
|
||||
/// numeric-compatible resolver as left/right operands, enabling powerful comparisons like "Attribute > Constant".
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private StatescriptGraph? _graph;
|
||||
private Action? _onChanged;
|
||||
|
||||
private OptionButton? _operationDropdown;
|
||||
private VBoxContainer? _leftContainer;
|
||||
private VBoxContainer? _rightContainer;
|
||||
private OptionButton? _leftResolverDropdown;
|
||||
private OptionButton? _rightResolverDropdown;
|
||||
|
||||
private NodeEditorProperty? _leftEditor;
|
||||
private NodeEditorProperty? _rightEditor;
|
||||
|
||||
private List<Func<NodeEditorProperty>> _numericFactories = [];
|
||||
private ComparisonOperation _operation;
|
||||
|
||||
private VBoxContainer? _leftEditorContainer;
|
||||
private VBoxContainer? _rightEditorContainer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Comparison";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Comparison";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(bool) || expectedType == typeof(ForgeVariant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_graph = graph;
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
_numericFactories = StatescriptResolverRegistry.GetCompatibleFactories(typeof(ForgeVariant128));
|
||||
|
||||
_numericFactories.RemoveAll(x =>
|
||||
{
|
||||
using NodeEditorProperty temp = x();
|
||||
return temp.ResolverTypeId == "Comparison";
|
||||
});
|
||||
|
||||
var comparisonResolver = property?.Resolver as ComparisonResolverResource;
|
||||
|
||||
if (comparisonResolver is not null)
|
||||
{
|
||||
_operation = comparisonResolver.Operation;
|
||||
}
|
||||
|
||||
var leftFoldable = new FoldableContainer { Title = "Left:" };
|
||||
leftFoldable.FoldingChanged += OnFoldingChanged;
|
||||
vBox.AddChild(leftFoldable);
|
||||
|
||||
_leftContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
leftFoldable.AddChild(_leftContainer);
|
||||
|
||||
_leftEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_leftResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Left);
|
||||
_leftContainer.AddChild(_leftResolverDropdown);
|
||||
_leftContainer.AddChild(_leftEditorContainer);
|
||||
ShowNestedEditor(
|
||||
GetSelectedIndex(comparisonResolver?.Left),
|
||||
comparisonResolver?.Left,
|
||||
_leftEditorContainer,
|
||||
x => _leftEditor = x);
|
||||
|
||||
_leftResolverDropdown.ItemSelected += OnLeftResolverDropdownItemSelected;
|
||||
|
||||
var opRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(opRow);
|
||||
|
||||
opRow.AddChild(new Label { Text = "Op:" });
|
||||
|
||||
_operationDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
_operationDropdown.AddItem("==");
|
||||
_operationDropdown.AddItem("!=");
|
||||
_operationDropdown.AddItem("<");
|
||||
_operationDropdown.AddItem("<=");
|
||||
_operationDropdown.AddItem(">");
|
||||
_operationDropdown.AddItem(">=");
|
||||
|
||||
_operationDropdown.Selected = (int)_operation;
|
||||
|
||||
_operationDropdown.ItemSelected += OnOperationDropdownItemSelected;
|
||||
|
||||
opRow.AddChild(_operationDropdown);
|
||||
|
||||
var rightFoldable = new FoldableContainer { Title = "Right:" };
|
||||
rightFoldable.FoldingChanged += OnFoldingChanged;
|
||||
vBox.AddChild(rightFoldable);
|
||||
|
||||
_rightContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
rightFoldable.AddChild(_rightContainer);
|
||||
|
||||
_rightEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_rightResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Right);
|
||||
_rightContainer.AddChild(_rightResolverDropdown);
|
||||
_rightContainer.AddChild(_rightEditorContainer);
|
||||
ShowNestedEditor(
|
||||
GetSelectedIndex(comparisonResolver?.Right),
|
||||
comparisonResolver?.Right,
|
||||
_rightEditorContainer,
|
||||
x => _rightEditor = x);
|
||||
|
||||
_rightResolverDropdown.ItemSelected += OnRightResolverDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
var comparisonResolver = new ComparisonResolverResource { Operation = _operation };
|
||||
|
||||
if (_leftEditor is not null)
|
||||
{
|
||||
var leftProperty = new StatescriptNodeProperty();
|
||||
_leftEditor.SaveTo(leftProperty);
|
||||
comparisonResolver.Left = leftProperty.Resolver;
|
||||
}
|
||||
|
||||
if (_rightEditor is not null)
|
||||
{
|
||||
var rightProperty = new StatescriptNodeProperty();
|
||||
_rightEditor.SaveTo(rightProperty);
|
||||
comparisonResolver.Right = rightProperty.Resolver;
|
||||
}
|
||||
|
||||
property.Resolver = comparisonResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
|
||||
_leftEditor?.ClearCallbacks();
|
||||
_rightEditor?.ClearCallbacks();
|
||||
}
|
||||
|
||||
private void OnFoldingChanged(bool isFolded)
|
||||
{
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void OnOperationDropdownItemSelected(long x)
|
||||
{
|
||||
_operation = (ComparisonOperation)(int)x;
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnLeftResolverDropdownItemSelected(long x)
|
||||
{
|
||||
HandleResolverDropdownChanged((int)x, _leftEditorContainer, editor => _leftEditor = editor);
|
||||
}
|
||||
|
||||
private void OnRightResolverDropdownItemSelected(long x)
|
||||
{
|
||||
HandleResolverDropdownChanged((int)x, _rightEditorContainer, editor => _rightEditor = editor);
|
||||
}
|
||||
|
||||
private void HandleResolverDropdownChanged(
|
||||
int selectedIndex,
|
||||
VBoxContainer? editorContainer,
|
||||
Action<NodeEditorProperty?> setEditor)
|
||||
{
|
||||
if (editorContainer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in editorContainer.GetChildren())
|
||||
{
|
||||
editorContainer.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
|
||||
setEditor(null);
|
||||
ShowNestedEditor(selectedIndex, null, editorContainer, setEditor);
|
||||
_onChanged?.Invoke();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private int GetSelectedIndex(StatescriptResolverResource? existingResolver)
|
||||
{
|
||||
var selectedIndex = 0;
|
||||
|
||||
if (existingResolver is not null)
|
||||
{
|
||||
var existingTypeId = existingResolver.ResolverTypeId;
|
||||
|
||||
for (var i = 0; i < _numericFactories.Count; i++)
|
||||
{
|
||||
using NodeEditorProperty temp = _numericFactories[i]();
|
||||
|
||||
if (temp.ResolverTypeId == existingTypeId)
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
private OptionButton CreateResolverDropdownControl(StatescriptResolverResource? existingResolver)
|
||||
{
|
||||
var dropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
foreach (Func<NodeEditorProperty> factory in _numericFactories)
|
||||
{
|
||||
using NodeEditorProperty temp = factory();
|
||||
dropdown.AddItem(temp.DisplayName);
|
||||
}
|
||||
|
||||
dropdown.Selected = GetSelectedIndex(existingResolver);
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private void ShowNestedEditor(
|
||||
int factoryIndex,
|
||||
StatescriptResolverResource? existingResolver,
|
||||
VBoxContainer container,
|
||||
Action<NodeEditorProperty?> setEditor)
|
||||
{
|
||||
if (_graph is null || factoryIndex < 0 || factoryIndex >= _numericFactories.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NodeEditorProperty editor = _numericFactories[factoryIndex]();
|
||||
|
||||
StatescriptNodeProperty? tempProperty = null;
|
||||
|
||||
if (existingResolver is not null)
|
||||
{
|
||||
tempProperty = new StatescriptNodeProperty { Resolver = existingResolver };
|
||||
}
|
||||
|
||||
editor.Setup(_graph, tempProperty, typeof(ForgeVariant128), OnNestedEditorChanged, false);
|
||||
|
||||
editor.LayoutSizeChanged += RaiseLayoutSizeChanged;
|
||||
|
||||
container.AddChild(editor);
|
||||
setEditor(editor);
|
||||
}
|
||||
|
||||
private void OnNestedEditorChanged()
|
||||
{
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8uywbj8s8brq
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor for the ability activation magnitude. No configuration is needed, it simply reads the magnitude from
|
||||
/// the <see cref="Abilities.AbilityBehaviorContext"/> at runtime. Only compatible with <see langword="float"/> inputs.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class MagnitudeResolverEditor : NodeEditorProperty
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Magnitude";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Magnitude";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(float) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = "Ability Magnitude",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(label);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new MagnitudeResolverResource();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdsw31atjur88
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds a node input property to a shared variable on the owning entity. Uses a two-step
|
||||
/// selection: first select the <see cref="ForgeSharedVariableSet"/> resource, then select a compatible variable from
|
||||
/// that set. At runtime the value is read from the entity's <see cref="GraphContext.SharedVariables"/> bag.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class SharedVariableResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _setPaths = [];
|
||||
private readonly List<string> _setDisplayNames = [];
|
||||
private readonly List<string> _variableNames = [];
|
||||
|
||||
private OptionButton? _setDropdown;
|
||||
private OptionButton? _variableDropdown;
|
||||
private Action? _onChanged;
|
||||
private Type _expectedType = typeof(Variant128);
|
||||
|
||||
private string _selectedSetPath = string.Empty;
|
||||
private string _selectedVariableName = string.Empty;
|
||||
private StatescriptVariableType _selectedVariableType = StatescriptVariableType.Int;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Shared Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "SharedVariable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
_expectedType = expectedType;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is SharedVariableResolverResource sharedRes)
|
||||
{
|
||||
_selectedSetPath = sharedRes.SharedVariableSetPath;
|
||||
_selectedVariableName = sharedRes.VariableName;
|
||||
_selectedVariableType = sharedRes.VariableType;
|
||||
}
|
||||
|
||||
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(setRow);
|
||||
|
||||
setRow.AddChild(new Label
|
||||
{
|
||||
Text = "Set:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateSetDropdown();
|
||||
setRow.AddChild(_setDropdown);
|
||||
|
||||
var varRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(varRow);
|
||||
|
||||
varRow.AddChild(new Label
|
||||
{
|
||||
Text = "Var:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_variableDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateVariableDropdown();
|
||||
varRow.AddChild(_variableDropdown);
|
||||
|
||||
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
|
||||
_variableDropdown.ItemSelected += OnVariableDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new SharedVariableResolverResource
|
||||
{
|
||||
SharedVariableSetPath = _selectedSetPath,
|
||||
VariableName = _selectedVariableName,
|
||||
VariableType = _selectedVariableType,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private static List<string> FindAllSharedVariableSetPaths()
|
||||
{
|
||||
var results = new List<string>();
|
||||
EditorFileSystemDirectory root = EditorInterface.Singleton.GetResourceFilesystem().GetFilesystem();
|
||||
ScanFilesystemDirectory(root, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void ScanFilesystemDirectory(EditorFileSystemDirectory dir, List<string> results)
|
||||
{
|
||||
for (var i = 0; i < dir.GetFileCount(); i++)
|
||||
{
|
||||
var path = dir.GetFilePath(i);
|
||||
|
||||
if (!path.EndsWith(".tres", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& !path.EndsWith(".res", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Resource resource = ResourceLoader.Load(path);
|
||||
|
||||
if (resource is ForgeSharedVariableSet)
|
||||
{
|
||||
results.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < dir.GetSubdirCount(); i++)
|
||||
{
|
||||
ScanFilesystemDirectory(dir.GetSubdir(i), results);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSetDropdownItemSelected(long index)
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _setDropdown.Selected;
|
||||
_selectedSetPath = idx >= 0 && idx < _setPaths.Count ? _setPaths[idx] : string.Empty;
|
||||
_selectedVariableName = string.Empty;
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
|
||||
PopulateVariableDropdown();
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnVariableDropdownItemSelected(long index)
|
||||
{
|
||||
if (_variableDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _variableDropdown.Selected;
|
||||
|
||||
if (idx >= 0 && idx < _variableNames.Count)
|
||||
{
|
||||
_selectedVariableName = _variableNames[idx];
|
||||
ResolveVariableType();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedVariableName = string.Empty;
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateSetDropdown()
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setDropdown.Clear();
|
||||
_setPaths.Clear();
|
||||
_setDisplayNames.Clear();
|
||||
|
||||
_setDropdown.AddItem("(None)");
|
||||
_setPaths.Add(string.Empty);
|
||||
_setDisplayNames.Add("(None)");
|
||||
|
||||
foreach (var path in FindAllSharedVariableSetPaths())
|
||||
{
|
||||
var displayName = path[(path.LastIndexOf('/') + 1)..];
|
||||
|
||||
if (displayName.EndsWith(".tres", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
displayName = displayName[..^5];
|
||||
}
|
||||
|
||||
_setDropdown.AddItem(displayName);
|
||||
_setPaths.Add(path);
|
||||
_setDisplayNames.Add(displayName);
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _setPaths.Count; i++)
|
||||
{
|
||||
if (_setPaths[i] == _selectedSetPath)
|
||||
{
|
||||
_setDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_setDropdown.Selected = 0;
|
||||
_selectedSetPath = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateVariableDropdown()
|
||||
{
|
||||
if (_variableDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_variableDropdown.Clear();
|
||||
_variableNames.Clear();
|
||||
|
||||
_variableDropdown.AddItem("(None)");
|
||||
_variableNames.Add(string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedSetPath) && ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is not null)
|
||||
{
|
||||
foreach (ForgeSharedVariableDefinition def in set.Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(def.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_expectedType != typeof(Variant128)
|
||||
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, def.VariableType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var label = $"{def.VariableName}";
|
||||
_variableDropdown.AddItem(label);
|
||||
_variableNames.Add(def.VariableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _variableNames.Count; i++)
|
||||
{
|
||||
if (_variableNames[i] == _selectedVariableName)
|
||||
{
|
||||
_variableDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_variableDropdown.Selected = 0;
|
||||
_selectedVariableName = string.Empty;
|
||||
}
|
||||
|
||||
private void ResolveVariableType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedSetPath)
|
||||
|| string.IsNullOrEmpty(_selectedVariableName)
|
||||
|| !ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is null)
|
||||
{
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ForgeSharedVariableDefinition def in set.Variables)
|
||||
{
|
||||
if (def.VariableName == _selectedVariableName)
|
||||
{
|
||||
_selectedVariableType = def.VariableType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://55ynvr5cbscp
|
||||
198
addons/forge/editor/statescript/resolvers/TagResolverEditor.cs
Normal file
198
addons/forge/editor/statescript/resolvers/TagResolverEditor.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that selects a single tag. Reuses the tag tree UI pattern from <c>TagEditorProperty</c>.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class TagResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private Button? _tagButton;
|
||||
private ScrollContainer? _scroll;
|
||||
private Tree? _tree;
|
||||
private string _selectedTag = string.Empty;
|
||||
private Texture2D? _checkedIcon;
|
||||
private Texture2D? _uncheckedIcon;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Tag";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Tag";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(bool) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
// Restore from existing binding.
|
||||
if (property?.Resolver is TagResolverResource tagRes)
|
||||
{
|
||||
_selectedTag = tagRes.Tag;
|
||||
}
|
||||
|
||||
_checkedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioChecked", "EditorIcons");
|
||||
|
||||
_uncheckedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioUnchecked", "EditorIcons");
|
||||
|
||||
_tagButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag,
|
||||
};
|
||||
|
||||
_tagButton.Toggled += OnTagButtonToggled;
|
||||
|
||||
vBox.AddChild(_tagButton);
|
||||
|
||||
_scroll = new ScrollContainer
|
||||
{
|
||||
Visible = false,
|
||||
CustomMinimumSize = new Vector2(0, 180),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_scroll.AddChild(_tree);
|
||||
vBox.AddChild(_scroll);
|
||||
|
||||
_tree.ButtonClicked += OnTreeButtonClicked;
|
||||
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new TagResolverResource
|
||||
{
|
||||
Tag = _selectedTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnTagButtonToggled(bool toggled)
|
||||
{
|
||||
if (_scroll is not null)
|
||||
{
|
||||
_scroll.Visible = toggled;
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTreeButtonClicked(TreeItem item, long column, long id, long mouseButton)
|
||||
{
|
||||
if (mouseButton != 1 || id != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_treeItemToNode.TryGetValue(item, out TagNode? tagNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Forge.Core.StringKey newValue = tagNode.CompleteTagKey;
|
||||
|
||||
if (newValue == _selectedTag)
|
||||
{
|
||||
newValue = string.Empty;
|
||||
}
|
||||
|
||||
_selectedTag = newValue;
|
||||
|
||||
if (_tagButton is not null)
|
||||
{
|
||||
_tagButton.Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag;
|
||||
}
|
||||
|
||||
RebuildTree();
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tree.Clear();
|
||||
_treeItemToNode.Clear();
|
||||
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
BuildTreeRecursive(root, tagsManager.RootNode);
|
||||
}
|
||||
|
||||
private void BuildTreeRecursive(TreeItem parent, TagNode node)
|
||||
{
|
||||
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TagNode child in node.ChildTags)
|
||||
{
|
||||
TreeItem item = _tree.CreateItem(parent);
|
||||
item.SetText(0, child.TagKey);
|
||||
|
||||
var selected = _selectedTag == child.CompleteTagKey;
|
||||
item.AddButton(0, selected ? _checkedIcon : _uncheckedIcon);
|
||||
|
||||
_treeItemToNode[item] = child;
|
||||
BuildTreeRecursive(item, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://drl073r6x5m16
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds an input property to a graph variable. Only variables whose type is compatible with the
|
||||
/// expected type are shown in the dropdown.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VariableResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _variableNames = [];
|
||||
|
||||
private OptionButton? _dropdown;
|
||||
private string _selectedVariableName = string.Empty;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
CustomMinimumSize = new Vector2(200, 25);
|
||||
|
||||
_dropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(100, 0),
|
||||
};
|
||||
|
||||
_dropdown.SetMeta("is_variable_dropdown", true);
|
||||
|
||||
PopulateDropdown(graph, expectedType);
|
||||
|
||||
if (property?.Resolver is VariableResolverResource varRes
|
||||
&& !string.IsNullOrEmpty(varRes.VariableName))
|
||||
{
|
||||
_selectedVariableName = varRes.VariableName;
|
||||
SelectByName(varRes.VariableName);
|
||||
}
|
||||
|
||||
_dropdown.ItemSelected += OnDropdownItemSelected;
|
||||
|
||||
AddChild(_dropdown);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new VariableResolverResource
|
||||
{
|
||||
VariableName = _selectedVariableName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnDropdownItemSelected(long index)
|
||||
{
|
||||
if (_dropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _dropdown.Selected;
|
||||
_selectedVariableName = idx >= 0 && idx < _variableNames.Count ? _variableNames[idx] : string.Empty;
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateDropdown(StatescriptGraph graph, Type expectedType)
|
||||
{
|
||||
if (_dropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dropdown.Clear();
|
||||
_variableNames.Clear();
|
||||
|
||||
_dropdown.AddItem("(None)");
|
||||
_variableNames.Add(string.Empty);
|
||||
|
||||
foreach (StatescriptGraphVariable variable in graph.Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(variable.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!StatescriptVariableTypeConverter.IsCompatible(expectedType, variable.VariableType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_dropdown.AddItem(variable.VariableName);
|
||||
_variableNames.Add(variable.VariableName);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectByName(string name)
|
||||
{
|
||||
if (_dropdown is null || string.IsNullOrEmpty(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _variableNames.Count; i++)
|
||||
{
|
||||
if (_variableNames[i] == name)
|
||||
{
|
||||
_dropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedVariableName = string.Empty;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cs7v6x0xv1a3k
|
||||
@@ -0,0 +1,388 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using GodotVariant = Godot.Variant;
|
||||
using GodotVector2 = Godot.Vector2;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that holds a constant (inline) value. The user edits the value directly in the node.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VariantResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private StatescriptVariableType _valueType;
|
||||
private bool _isArray;
|
||||
private bool _isArrayExpanded;
|
||||
private GodotVariant _currentValue;
|
||||
private Array<GodotVariant> _arrayValues = [];
|
||||
private Action? _onChanged;
|
||||
|
||||
private Button? _toggleButton;
|
||||
private VBoxContainer? _elementsContainer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Constant";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Variant";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_isArray = isArray;
|
||||
_onChanged = onChanged;
|
||||
|
||||
if (!StatescriptVariableTypeConverter.TryFromSystemType(expectedType, out _valueType))
|
||||
{
|
||||
_valueType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
if (property?.Resolver is VariantResolverResource variantRes)
|
||||
{
|
||||
_valueType = variantRes.ValueType;
|
||||
|
||||
if (_isArray)
|
||||
{
|
||||
_arrayValues = [.. variantRes.ArrayValues];
|
||||
_isArrayExpanded = variantRes.IsArrayExpanded;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentValue = variantRes.Value;
|
||||
}
|
||||
}
|
||||
else if (_isArray)
|
||||
{
|
||||
_arrayValues = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
|
||||
}
|
||||
|
||||
CustomMinimumSize = new GodotVector2(200, 40);
|
||||
|
||||
if (_isArray)
|
||||
{
|
||||
VBoxContainer arrayEditor = CreateArrayEditor();
|
||||
AddChild(arrayEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Control valueEditor = CreateValueEditor();
|
||||
AddChild(valueEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
if (_isArray)
|
||||
{
|
||||
property.Resolver = new VariantResolverResource
|
||||
{
|
||||
ValueType = _valueType,
|
||||
IsArray = true,
|
||||
ArrayValues = [.. _arrayValues],
|
||||
IsArrayExpanded = _isArrayExpanded,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
property.Resolver = new VariantResolverResource
|
||||
{
|
||||
Value = _currentValue,
|
||||
ValueType = _valueType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private Control CreateValueEditor()
|
||||
{
|
||||
if (_valueType == StatescriptVariableType.Bool)
|
||||
{
|
||||
return StatescriptEditorControls.CreateBoolEditor(_currentValue.AsBool(), OnBoolValueChanged);
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsIntegerType(_valueType)
|
||||
|| StatescriptEditorControls.IsFloatType(_valueType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
_valueType,
|
||||
_currentValue.AsDouble(),
|
||||
OnNumericValueChanged);
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(_valueType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateVectorEditor(
|
||||
_valueType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(_currentValue, _valueType, x),
|
||||
OnVectorValueChanged);
|
||||
}
|
||||
|
||||
return new Label { Text = _valueType.ToString() };
|
||||
}
|
||||
|
||||
private void OnBoolValueChanged(bool x)
|
||||
{
|
||||
_currentValue = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnNumericValueChanged(double x)
|
||||
{
|
||||
_currentValue = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnVectorValueChanged(double[] x)
|
||||
{
|
||||
_currentValue = StatescriptEditorControls.BuildVectorVariant(_valueType, x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private VBoxContainer CreateArrayEditor()
|
||||
{
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
_elementsContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Visible = _isArrayExpanded,
|
||||
};
|
||||
|
||||
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(headerRow);
|
||||
|
||||
_toggleButton = new Button
|
||||
{
|
||||
Text = $"Array (size {_arrayValues.Count})",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = _isArrayExpanded,
|
||||
};
|
||||
|
||||
_toggleButton.Toggled += OnArrayToggled;
|
||||
|
||||
headerRow.AddChild(_toggleButton);
|
||||
|
||||
Texture2D addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
|
||||
var addButton = new Button
|
||||
{
|
||||
Icon = addIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Add Element",
|
||||
CustomMinimumSize = new GodotVector2(24, 24),
|
||||
};
|
||||
|
||||
addButton.Pressed += OnAddElementPressed;
|
||||
|
||||
headerRow.AddChild(addButton);
|
||||
|
||||
vBox.AddChild(_elementsContainer);
|
||||
|
||||
RebuildArrayElements();
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private void OnArrayToggled(bool toggled)
|
||||
{
|
||||
if (_elementsContainer is not null)
|
||||
{
|
||||
_elementsContainer.Visible = toggled;
|
||||
}
|
||||
|
||||
_isArrayExpanded = toggled;
|
||||
_onChanged?.Invoke();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void OnAddElementPressed()
|
||||
{
|
||||
GodotVariant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
|
||||
_arrayValues.Add(defaultValue);
|
||||
_onChanged?.Invoke();
|
||||
|
||||
if (_elementsContainer is not null)
|
||||
{
|
||||
_elementsContainer.Visible = true;
|
||||
}
|
||||
|
||||
_isArrayExpanded = true;
|
||||
RebuildArrayElements();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void RebuildArrayElements()
|
||||
{
|
||||
if (_elementsContainer is null || _toggleButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in _elementsContainer.GetChildren())
|
||||
{
|
||||
_elementsContainer.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
|
||||
_toggleButton.Text = $"Array (size {_arrayValues.Count})";
|
||||
|
||||
Texture2D removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
for (var i = 0; i < _arrayValues.Count; i++)
|
||||
{
|
||||
var capturedIndex = i;
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(_valueType))
|
||||
{
|
||||
var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_elementsContainer.AddChild(elementVBox);
|
||||
|
||||
var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementVBox.AddChild(labelRow);
|
||||
labelRow.AddChild(new Label
|
||||
{
|
||||
Text = $"[{i}]",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
AddArrayRemoveButton(labelRow, removeIcon, capturedIndex);
|
||||
|
||||
VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor(
|
||||
_valueType,
|
||||
x =>
|
||||
{
|
||||
return StatescriptEditorControls.GetVectorComponent(
|
||||
_arrayValues[capturedIndex],
|
||||
_valueType,
|
||||
x);
|
||||
},
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] =
|
||||
StatescriptEditorControls.BuildVectorVariant(_valueType, x);
|
||||
_onChanged?.Invoke();
|
||||
});
|
||||
|
||||
elementVBox.AddChild(vectorEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
if (_valueType == StatescriptVariableType.Bool)
|
||||
{
|
||||
elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
_arrayValues[capturedIndex].AsBool(),
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
_valueType,
|
||||
_arrayValues[capturedIndex].AsDouble(),
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
});
|
||||
|
||||
elementRow.AddChild(spin);
|
||||
}
|
||||
|
||||
AddArrayRemoveButton(elementRow, removeIcon, capturedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddArrayRemoveButton(
|
||||
HBoxContainer row,
|
||||
Texture2D removeIcon,
|
||||
int elementIndex)
|
||||
{
|
||||
var removeButton = new Button
|
||||
{
|
||||
Icon = removeIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Remove Element",
|
||||
CustomMinimumSize = new GodotVector2(24, 24),
|
||||
};
|
||||
|
||||
var handler = new ArrayRemoveHandler(this, elementIndex);
|
||||
removeButton.AddChild(handler);
|
||||
removeButton.Pressed += handler.HandlePressed;
|
||||
|
||||
row.AddChild(removeButton);
|
||||
}
|
||||
|
||||
private void OnRemoveElement(int elementIndex)
|
||||
{
|
||||
_arrayValues.RemoveAt(elementIndex);
|
||||
_onChanged?.Invoke();
|
||||
RebuildArrayElements();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Godot-compatible signal handler for array element remove buttons. Holds the element index and a reference to the
|
||||
/// owning editor so the <c>Pressed</c> signal can be handled without a lambda.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
private sealed partial class ArrayRemoveHandler : Node
|
||||
{
|
||||
private readonly VariantResolverEditor _editor;
|
||||
private readonly int _elementIndex;
|
||||
|
||||
public ArrayRemoveHandler()
|
||||
{
|
||||
_editor = null!;
|
||||
}
|
||||
|
||||
public ArrayRemoveHandler(VariantResolverEditor editor, int elementIndex)
|
||||
{
|
||||
_editor = editor;
|
||||
_elementIndex = elementIndex;
|
||||
}
|
||||
|
||||
public void HandlePressed()
|
||||
{
|
||||
_editor.OnRemoveElement(_elementIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv2fk6v67mt3u
|
||||
@@ -11,7 +11,7 @@ using GodotStringArray = Godot.Collections.Array<string>;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
[Tool]
|
||||
public partial class TagContainerEditorProperty : EditorProperty
|
||||
public partial class TagContainerEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
@@ -84,6 +84,20 @@ public partial class TagContainerEditorProperty : EditorProperty
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
for (var i = GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = GetChild(i);
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
_tree.Clear();
|
||||
@@ -95,7 +109,7 @@ public partial class TagContainerEditorProperty : EditorProperty
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData =
|
||||
ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
|
||||
var tagsManager =
|
||||
new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
@@ -9,7 +9,7 @@ using Godot;
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
[Tool]
|
||||
public partial class TagEditorProperty : EditorProperty
|
||||
public partial class TagEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
@@ -80,6 +80,20 @@ public partial class TagEditorProperty : EditorProperty
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
for (var i = GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = GetChild(i);
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
_tree.Clear();
|
||||
@@ -91,7 +105,7 @@ public partial class TagEditorProperty : EditorProperty
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData =
|
||||
ResourceLoader.Load<ForgeData>("uid://8j4xg16o3qnl");
|
||||
ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
|
||||
var tagsManager =
|
||||
new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
257
addons/forge/editor/tags/TagsEditorDock.cs
Normal file
257
addons/forge/editor/tags/TagsEditorDock.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Editor dock for managing gameplay tags.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
public partial class TagsEditorDock : EditorDock, ISerializationListener
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private TagsManager _tagsManager = null!;
|
||||
|
||||
private ForgeData? _forgePluginData;
|
||||
|
||||
private Tree? _tree;
|
||||
private LineEdit? _tagNameTextField;
|
||||
private Button? _addTagButton;
|
||||
|
||||
private Texture2D? _addIcon;
|
||||
private Texture2D? _removeIcon;
|
||||
|
||||
public TagsEditorDock()
|
||||
{
|
||||
Title = "Tags";
|
||||
DockIcon = GD.Load<Texture2D>("uid://cu6ncpuumjo20");
|
||||
DefaultSlot = DockSlot.RightUl;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
_forgePluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
|
||||
_addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
_removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
BuildUI();
|
||||
ConstructTagTree();
|
||||
|
||||
_tree!.ButtonClicked += TreeButtonClicked;
|
||||
_addTagButton!.Pressed += AddTagButton_Pressed;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
if (_tree is not null)
|
||||
{
|
||||
_tree.ButtonClicked -= TreeButtonClicked;
|
||||
}
|
||||
|
||||
if (_addTagButton is not null)
|
||||
{
|
||||
_addTagButton.Pressed -= AddTagButton_Pressed;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
if (_tree is not null)
|
||||
{
|
||||
_tree.ButtonClicked += TreeButtonClicked;
|
||||
}
|
||||
|
||||
if (_addTagButton is not null)
|
||||
{
|
||||
_addTagButton.Pressed += AddTagButton_Pressed;
|
||||
}
|
||||
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
var vBox = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(vBox);
|
||||
|
||||
var hBox = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
vBox.AddChild(hBox);
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = "Tag Name:",
|
||||
};
|
||||
|
||||
hBox.AddChild(label);
|
||||
|
||||
_tagNameTextField = new LineEdit
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
hBox.AddChild(_tagNameTextField);
|
||||
|
||||
_addTagButton = new Button
|
||||
{
|
||||
Text = "Add Tag",
|
||||
};
|
||||
|
||||
hBox.AddChild(_addTagButton);
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
vBox.AddChild(_tree);
|
||||
}
|
||||
|
||||
private void AddTagButton_Pressed()
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
if (!Tag.IsValidKey(_tagNameTextField.Text, out var _, out var fixedTag))
|
||||
{
|
||||
_tagNameTextField.Text = fixedTag;
|
||||
}
|
||||
|
||||
if (_forgePluginData.RegisteredTags.Contains(_tagNameTextField.Text))
|
||||
{
|
||||
GD.PushWarning($"Tag [{_tagNameTextField.Text}] is already present in the manager.");
|
||||
return;
|
||||
}
|
||||
|
||||
_forgePluginData.RegisteredTags.Add(_tagNameTextField.Text);
|
||||
ResourceSaver.Save(_forgePluginData);
|
||||
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
|
||||
private void ReconstructTreeNode()
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
_tagsManager.DestroyTagTree();
|
||||
_tagsManager = new TagsManager([.. _forgePluginData.RegisteredTags]);
|
||||
|
||||
_tree.Clear();
|
||||
ConstructTagTree();
|
||||
}
|
||||
|
||||
private void ConstructTagTree()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
TreeItem rootTreeNode = _tree.CreateItem();
|
||||
_tree.HideRoot = true;
|
||||
|
||||
if (_tagsManager.RootNode.ChildTags.Count == 0)
|
||||
{
|
||||
TreeItem childTreeNode = _tree.CreateItem(rootTreeNode);
|
||||
childTreeNode.SetText(0, "No tag has been registered yet.");
|
||||
childTreeNode.SetCustomColor(0, Color.FromHtml("EED202"));
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTreeRecursively(_tree, rootTreeNode, _tagsManager.RootNode);
|
||||
}
|
||||
|
||||
private void BuildTreeRecursively(Tree tree, TreeItem currentTreeItem, TagNode currentNode)
|
||||
{
|
||||
foreach (TagNode childTagNode in currentNode.ChildTags)
|
||||
{
|
||||
TreeItem childTreeNode = tree.CreateItem(currentTreeItem);
|
||||
childTreeNode.SetText(0, childTagNode.TagKey);
|
||||
childTreeNode.AddButton(0, _addIcon);
|
||||
childTreeNode.AddButton(0, _removeIcon);
|
||||
|
||||
_treeItemToNode.Add(childTreeNode, childTagNode);
|
||||
|
||||
BuildTreeRecursively(tree, childTreeNode, childTagNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void TreeButtonClicked(TreeItem item, long column, long id, long mouseButtonIndex)
|
||||
{
|
||||
EnsureInitialized();
|
||||
Debug.Assert(
|
||||
_forgePluginData.RegisteredTags is not null,
|
||||
$"{_forgePluginData.RegisteredTags} should have been initialized by the Forge plugin.");
|
||||
|
||||
if (mouseButtonIndex == 1)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
_tagNameTextField.Text = $"{_treeItemToNode[item].CompleteTagKey}.";
|
||||
_tagNameTextField.GrabFocus();
|
||||
_tagNameTextField.CaretColumn = _tagNameTextField.Text.Length;
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
TagNode selectedTag = _treeItemToNode[item];
|
||||
|
||||
for (var i = _forgePluginData.RegisteredTags.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var tag = _forgePluginData.RegisteredTags[i];
|
||||
|
||||
if (string.Equals(tag, selectedTag.CompleteTagKey, StringComparison.OrdinalIgnoreCase) ||
|
||||
tag.StartsWith(selectedTag.CompleteTagKey + ".", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_forgePluginData.RegisteredTags.Remove(tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTag.ParentTagNode is not null
|
||||
&& !_forgePluginData.RegisteredTags.Contains(selectedTag.ParentTagNode.CompleteTagKey))
|
||||
{
|
||||
_forgePluginData.RegisteredTags.Add(selectedTag.ParentTagNode.CompleteTagKey);
|
||||
}
|
||||
|
||||
ResourceSaver.Save(_forgePluginData);
|
||||
ReconstructTreeNode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_tree), nameof(_tagNameTextField), nameof(_forgePluginData))]
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
Debug.Assert(_tree is not null, $"{_tree} should have been initialized on _Ready().");
|
||||
Debug.Assert(_tagNameTextField is not null, $"{_tagNameTextField} should have been initialized on _Ready().");
|
||||
Debug.Assert(_forgePluginData is not null, $"{_forgePluginData} should have been initialized on _Ready().");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/tags/TagsEditorDock.cs.uid
Normal file
1
addons/forge/editor/tags/TagsEditorDock.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://drgjhyxk7rkgg
|
||||
53
addons/forge/icons/Statescript.svg
Normal file
53
addons/forge/icons/Statescript.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="Statescript.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="77.4375"
|
||||
inkscape:cx="7.9935432"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="3432"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 11,10 a 1,1 0 0 0 -1,1 v 3 a 1,1 0 0 0 1,1 h 3 a 1,1 0 0 0 1,-1 v -3 a 1,1 0 0 0 -1,-1 z"
|
||||
id="path5"
|
||||
style="fill:#8eef97;fill-opacity:1" />
|
||||
<path
|
||||
d="M 7,8.883 V 10 A 2,2 0 0 1 6.732,11 L 9,12.117 V 11 a 2,2 0 0 1 0.268,-1 z"
|
||||
id="path4"
|
||||
style="fill:#d2d3d5;fill-opacity:1" />
|
||||
<path
|
||||
d="M 2,5 A 1,1 0 0 0 1,6 v 4 a 1,1 0 0 0 1,1 H 5 A 1,1 0 0 0 6,10 V 6 A 1,1 0 0 0 5,5 Z"
|
||||
id="path3"
|
||||
style="fill:#8da5f3;fill-opacity:1" />
|
||||
<path
|
||||
d="M 6.732,5 A 2,2 0 0 1 7,6 V 7.117 L 9.268,6 A 2,2 0 0 1 9,5 V 3.883 Z"
|
||||
id="path2"
|
||||
style="fill:#d2d3d5;fill-opacity:1" />
|
||||
<path
|
||||
d="m 11,1 a 1,1 0 0 0 -1,1 v 3 a 1,1 0 0 0 1,1 h 3 A 1,1 0 0 0 15,5 V 2 A 1,1 0 0 0 14,1 Z"
|
||||
id="path1"
|
||||
style="fill:#fc7f7f;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
43
addons/forge/icons/Statescript.svg.import
Normal file
43
addons/forge/icons/Statescript.svg.import
Normal file
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b6yrjb46fluw3"
|
||||
path="res://.godot/imported/Statescript.svg-09aa700bcff07f99651a8110f8eff0a6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/forge/icons/Statescript.svg"
|
||||
dest_files=["res://.godot/imported/Statescript.svg-09aa700bcff07f99651a8110f8eff0a6.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
|
||||
@@ -49,9 +49,11 @@
|
||||
<path
|
||||
fill="#5fff97"
|
||||
d="m 7.9265537,9.7974173 -2,-4 -2,4 z"
|
||||
id="path1-1" />
|
||||
id="path1-1"
|
||||
style="fill:#8eef97;fill-opacity:1" />
|
||||
<path
|
||||
fill="#ff5f5f"
|
||||
d="m 11.926554,5.7974173 -2.0000003,4 -2,-4 z"
|
||||
id="path2" />
|
||||
id="path2"
|
||||
style="fill:#fc7f7f;fill-opacity:1" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -24,20 +24,20 @@
|
||||
inkscape:zoom="38.71875"
|
||||
inkscape:cx="9.4527845"
|
||||
inkscape:cy="11.867635"
|
||||
inkscape:window-width="3408"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="24"
|
||||
inkscape:window-y="723"
|
||||
inkscape:window-x="3432"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
fill="#5fff97"
|
||||
d="M 7.9265536,11.734463 3.9895075,3.8603713 0.05246155,11.734463 Z"
|
||||
id="path1-1"
|
||||
style="stroke-width:1.96852" />
|
||||
style="stroke-width:1.96852;fill:#8eef97;fill-opacity:1" />
|
||||
<path
|
||||
fill="#ff5f5f"
|
||||
d="M 15.800646,3.8603713 11.8636,11.734463 7.9265536,3.8603713 Z"
|
||||
id="path2"
|
||||
style="stroke-width:1.96852" />
|
||||
style="stroke-width:1.96852;fill:#fc7f7f;fill-opacity:1" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -7,7 +7,9 @@ using Gamesmiths.Forge.Effects;
|
||||
using Gamesmiths.Forge.Events;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
using Node = Godot.Node;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Nodes;
|
||||
|
||||
@@ -18,6 +20,9 @@ public partial class ForgeEntity : Node, IForgeEntity
|
||||
[Export]
|
||||
public ForgeTagContainer BaseTags { get; set; } = new();
|
||||
|
||||
[Export]
|
||||
public ForgeSharedVariableSet? SharedVariableDefinitions { get; set; }
|
||||
|
||||
public EntityAttributes Attributes { get; set; } = null!;
|
||||
|
||||
public EntityTags Tags { get; set; } = null!;
|
||||
@@ -28,6 +33,8 @@ public partial class ForgeEntity : Node, IForgeEntity
|
||||
|
||||
public EventManager Events { get; set; } = null!;
|
||||
|
||||
public Variables SharedVariables { get; set; } = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
@@ -36,6 +43,9 @@ public partial class ForgeEntity : Node, IForgeEntity
|
||||
EffectsManager = new EffectsManager(this, ForgeManagers.Instance.CuesManager);
|
||||
Abilities = new EntityAbilities(this);
|
||||
Events = new EventManager();
|
||||
SharedVariables = new Variables();
|
||||
|
||||
SharedVariableDefinitions?.PopulateVariables(SharedVariables);
|
||||
|
||||
List<AttributeSet> attributeSetList = [];
|
||||
|
||||
@@ -63,5 +73,6 @@ public partial class ForgeEntity : Node, IForgeEntity
|
||||
base._Process(delta);
|
||||
|
||||
EffectsManager.UpdateEffects(delta);
|
||||
Abilities.UpdateAbilities(delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
name="Forge Gameplay System"
|
||||
description="A plugin for managing Gameplay Tags and Status Effects."
|
||||
author="Gamesmiths Guild"
|
||||
version="0.2.0"
|
||||
version="0.3.0"
|
||||
script="ForgePluginLoader.cs"
|
||||
|
||||
14
addons/forge/resources/ForgeActivationDataField.cs
Normal file
14
addons/forge/resources/ForgeActivationDataField.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a single field exposed by an <see cref="IActivationDataProvider"/>. Each field defines a name and type
|
||||
/// that graph nodes can bind to via the Activation Data resolver.
|
||||
/// </summary>
|
||||
/// <param name="FieldName">The name of this data field. This name is used as the graph variable name at runtime.
|
||||
/// </param>
|
||||
/// <param name="FieldType">The type of this data field.</param>
|
||||
public readonly record struct ForgeActivationDataField(string FieldName, StatescriptVariableType FieldType);
|
||||
1
addons/forge/resources/ForgeActivationDataField.cs.uid
Normal file
1
addons/forge/resources/ForgeActivationDataField.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cv4mcpd3ifglu
|
||||
@@ -37,7 +37,7 @@ public partial class ForgeEffectData : Resource
|
||||
private LevelComparison _levelOverridePolicy;
|
||||
|
||||
[Export]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = "New Effect";
|
||||
|
||||
[Export]
|
||||
public bool SnapshotLevel { get; set; } = true;
|
||||
|
||||
47
addons/forge/resources/ForgeSharedVariableDefinition.cs
Normal file
47
addons/forge/resources/ForgeSharedVariableDefinition.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Resource representing a single shared variable definition for an entity, including name, type, and initial value.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
[GlobalClass]
|
||||
public partial class ForgeSharedVariableDefinition : Resource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of this shared variable.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public string VariableName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of this shared variable.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public StatescriptVariableType VariableType { get; set; } = StatescriptVariableType.Int;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is an array variable.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public bool IsArray { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial value of this shared variable, stored as a Godot variant.
|
||||
/// For non-array variables, this is a single value. Ignored when <see cref="IsArray"/> is true.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Variant InitialValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial values for array variables.
|
||||
/// Each element is stored as a Godot variant. Only used when <see cref="IsArray"/> is true.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Array<Variant> InitialArrayValues { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://347pr45ke8ns
|
||||
65
addons/forge/resources/ForgeSharedVariableSet.cs
Normal file
65
addons/forge/resources/ForgeSharedVariableSet.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Resource containing a collection of shared variable definitions for an entity. Assign this to a
|
||||
/// <see cref="Nodes.ForgeEntity"/> (or custom <see cref="IForgeEntity"/> implementation) to define which shared
|
||||
/// variables the entity exposes at runtime.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
[GlobalClass]
|
||||
[Icon("uid://cu6ncpuumjo20")]
|
||||
public partial class ForgeSharedVariableSet : Resource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the shared variable definitions.
|
||||
/// </summary>
|
||||
[Export]
|
||||
public Array<ForgeSharedVariableDefinition> Variables { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Populates a <see cref="Variables"/> bag with all the definitions in this set, using each variable's name and
|
||||
/// initial value.
|
||||
/// </summary>
|
||||
/// <param name="target">The <see cref="Variables"/> instance to populate.</param>
|
||||
public void PopulateVariables(Variables target)
|
||||
{
|
||||
foreach (ForgeSharedVariableDefinition definition in Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(definition.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = new StringKey(definition.VariableName);
|
||||
|
||||
if (definition.IsArray)
|
||||
{
|
||||
var initialValues = new Variant128[definition.InitialArrayValues.Count];
|
||||
for (var i = 0; i < definition.InitialArrayValues.Count; i++)
|
||||
{
|
||||
initialValues[i] = StatescriptVariableTypeConverter.GodotVariantToForge(
|
||||
definition.InitialArrayValues[i],
|
||||
definition.VariableType);
|
||||
}
|
||||
|
||||
target.DefineArrayVariable(key, initialValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
Variant128 value = StatescriptVariableTypeConverter.GodotVariantToForge(
|
||||
definition.InitialValue,
|
||||
definition.VariableType);
|
||||
|
||||
target.DefineVariable(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user