Compare commits
78 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 | |||
| f3eea3f171 | |||
| 96b4fa7197 | |||
| 8d2fe1001e | |||
| 25aec40d03 | |||
| 80306bd095 | |||
| 9207295a99 | |||
| 4474ba22fa | |||
| 6101406f45 | |||
| c792c98ad2 | |||
| 74876a9a5d | |||
| bbb5149184 | |||
| 9e39528b9a | |||
| 1d2290b025 | |||
| b9ae83cd92 | |||
| 759d972b6d | |||
| 4df4585149 | |||
| 88b0911c6c | |||
| 81ce45a0dc | |||
| 1caf202310 | |||
| 22c4301244 | |||
| 032e059826 | |||
| a8683b77e8 | |||
| 90e6cdbcde | |||
| c09dfd1e7b | |||
| 9db0056c5d | |||
| 7f1d33e4fc | |||
| aaa9b102c1 | |||
| cb348667f5 | |||
| 8d23a95c68 | |||
| a9809abc27 | |||
| db65c02e75 | |||
| 5d49d6d681 | |||
| fa0e511b3a | |||
| 3efbd41f56 | |||
| 3148d3b69b | |||
| c4be97e0de |
@@ -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
|
||||
# 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:
|
||||
# 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/
|
||||
timeout: 1
|
||||
publish-report: false
|
||||
upload-report: false
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v3-node20
|
||||
with:
|
||||
name: Test Report
|
||||
path: ${{ github.workspace }}/reports/test-result.html
|
||||
# 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,28 +107,28 @@ 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
|
||||
zip -r Windows.zip build/windows
|
||||
ls -la build/windows
|
||||
|
||||
- name: Setup Butler
|
||||
shell: bash
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
run: |
|
||||
mkdir ./tools 2>/dev/null || true
|
||||
pushd tools
|
||||
curl -sSLfo ./butler.zip "https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default"
|
||||
unzip butler.zip
|
||||
chmod +x ./butler
|
||||
popd
|
||||
./tools/butler -V
|
||||
|
||||
- name: Upload to itch.io
|
||||
shell: bash
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
run: |
|
||||
versionArgument="--userversion ${{ needs.BumpTag.outputs.tag_name }}"
|
||||
./tools/butler push \
|
||||
"Windows.zip" \
|
||||
${{ env.ITCHIO_USERNAME }}/${{ env.ITCHIO_GAMEID }}:windows ${versionArgument}
|
||||
# - name: Setup Butler
|
||||
# shell: bash
|
||||
# env:
|
||||
# BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
# run: |
|
||||
# mkdir ./tools 2>/dev/null || true
|
||||
# pushd tools
|
||||
# curl -sSLfo ./butler.zip "https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default"
|
||||
# unzip butler.zip
|
||||
# chmod +x ./butler
|
||||
# popd
|
||||
# ./tools/butler -V
|
||||
#
|
||||
# - name: Upload to itch.io
|
||||
# shell: bash
|
||||
# env:
|
||||
# BUTLER_API_KEY: ${{ secrets.BUTLER_TOKEN }}
|
||||
# run: |
|
||||
# versionArgument="--userversion ${{ needs.BumpTag.outputs.tag_name }}"
|
||||
# ./tools/butler push \
|
||||
# "Windows.zip" \
|
||||
# ${{ env.ITCHIO_USERNAME }}/${{ env.ITCHIO_GAMEID }}:windows ${versionArgument}
|
||||
|
||||
|
||||
@@ -55,10 +55,15 @@ jobs:
|
||||
popd
|
||||
./tools/butler -V
|
||||
|
||||
- name: Remove GDUnit addon
|
||||
run: |
|
||||
rm -rf ${{ gitea.workspace }}/addons/gdUnit4
|
||||
|
||||
- name: Build Windows
|
||||
run: |
|
||||
mkdir -v -p build/windows
|
||||
godot --headless --verbose --build-solutions --export-release "Windows Desktop" build/windows/${{ env.GAME_NAME }}.exe
|
||||
${{ 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
|
||||
@@ -73,9 +78,9 @@ jobs:
|
||||
- name: Build Windows ARM
|
||||
run: |
|
||||
mkdir -v -p build/windowsArm
|
||||
godot --headless --verbose --build-solutions --export-release "Windows ARM" build/windowsArm/${{ env.GAME_NAME }}.exe
|
||||
${{ 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 }}
|
||||
@@ -88,9 +93,9 @@ jobs:
|
||||
- name: Linux Build
|
||||
run: |
|
||||
mkdir -v -p build/linux
|
||||
godot --headless --verbose --export-release "Linux/X11" build/linux/${{ env.GAME_NAME }}.x86_64
|
||||
${{ 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 }}
|
||||
@@ -103,9 +108,9 @@ jobs:
|
||||
- name: Mac Build
|
||||
run: |
|
||||
mkdir -v -p build/mac
|
||||
godot --headless --verbose --export-release "macOS" build/mac/${{ env.GAME_NAME }}.zip
|
||||
${{ 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
@@ -14,3 +14,16 @@
|
||||
|
||||
# Imported translations (automatically generated from CSV files)
|
||||
*.translation
|
||||
|
||||
docs/legal/
|
||||
|
||||
.output.txt
|
||||
|
||||
*.suo
|
||||
*.user
|
||||
*.csproj.old*
|
||||
_ReSharper.*
|
||||
*.DotSettings.user
|
||||
bin
|
||||
obj
|
||||
packages
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<TestSessionTimeout>180000</TestSessionTimeout>
|
||||
<TreatNoTestsAsError>true</TreatNoTestsAsError>
|
||||
<EnvironmentVariables>
|
||||
<GODOT_BIN>d:\development\Godot_v4.5-stable_mono_win64\Godot_v4.5-stable_mono_win64.exe</GODOT_BIN>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
|
||||
<LoggerRunSettings>
|
||||
|
||||
@@ -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,10 +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" />
|
||||
|
||||
<!-- 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,12 +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>
|
||||
9
addons/forge/Forge.props
Normal file
9
addons/forge/Forge.props
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gamesmiths.Forge" Version="0.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
274
addons/forge/ForgePluginLoader.cs
Normal file
274
addons/forge/ForgePluginLoader.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
// 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;
|
||||
|
||||
[Tool]
|
||||
public partial class ForgePluginLoader : EditorPlugin
|
||||
{
|
||||
private const string AutoloadPath = "uid://ba8fquhtwu5mu";
|
||||
|
||||
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()
|
||||
{
|
||||
EnsureForgeDataExists();
|
||||
|
||||
_tagsEditorDock = new TagsEditorDock();
|
||||
AddDock(_tagsEditorDock);
|
||||
|
||||
_tagContainerInspectorPlugin = new TagContainerInspectorPlugin();
|
||||
AddInspectorPlugin(_tagContainerInspectorPlugin);
|
||||
_tagInspectorPlugin = new TagInspectorPlugin();
|
||||
AddInspectorPlugin(_tagInspectorPlugin);
|
||||
_attributeSetInspectorPlugin = new AttributeSetInspectorPlugin();
|
||||
AddInspectorPlugin(_attributeSetInspectorPlugin);
|
||||
_cueHandlerInspectorPlugin = new CueHandlerInspectorPlugin();
|
||||
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(
|
||||
_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().");
|
||||
|
||||
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)
|
||||
{
|
||||
GD.PrintErr("Failed to load script at res://addons/forge/core/ForgeBootstrap.cs");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ProjectSettings.HasSetting("autoload/Forge Bootstrap"))
|
||||
{
|
||||
ProjectSettings.SetSetting("autoload/Forge Bootstrap", AutoloadPath);
|
||||
ProjectSettings.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _DisablePlugin()
|
||||
{
|
||||
if (ProjectSettings.HasSetting("autoload/Forge Bootstrap"))
|
||||
{
|
||||
ProjectSettings.Clear("autoload/Forge Bootstrap");
|
||||
ProjectSettings.Save();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
1
addons/forge/ForgePluginLoader.cs.uid
Normal file
1
addons/forge/ForgePluginLoader.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://686m2ah4as6w
|
||||
21
addons/forge/LICENSE
Normal file
21
addons/forge/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Gamesmiths Guild
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
61
addons/forge/README.md
Normal file
61
addons/forge/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Forge for Godot
|
||||
|
||||
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, cues, and visual ability scripting through Statescript, fully aligned with Godot’s node, resource, and editor workflows.
|
||||
|
||||
This plugin enables you to:
|
||||
|
||||
- Use **ForgeEntity** nodes or implement `IForgeEntity` to integrate core Forge systems like attributes, effects, abilities, events and tags.
|
||||
- Define attributes, effects, abilities, cues, and tags directly in the Godot editor.
|
||||
- Apply and manage gameplay effects with area or raycasting nodes.
|
||||
- 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
|
||||
|
||||
- **Effects System**: Comprehensive effect application and management, including stacking, periodic, instant, and infinite effects.
|
||||
- **Attributes System**: Attribute management, supporting sets, modifiers, and configuration.
|
||||
- **Tags System**: Full hierarchical tag system with Godot editor integration.
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
### Requirements
|
||||
|
||||
- Godot 4.6 or later with .NET support.
|
||||
- .NET SDK 8.0 or later.
|
||||
|
||||
### Steps
|
||||
|
||||
1. Install the plugin via the Godot Asset Library or manually by copying the `addons` folder.
|
||||
- [Godot Asset Library](https://godotengine.org/asset-library/asset/4239)
|
||||
- [Manual installation guide](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html)
|
||||
2. Add the following line in your `.csproj` file (before the closing `</Project>` tag). The `.csproj` file can be created through Godot by navigating to `Project > Tools > C# > Create C# solution`:
|
||||
```xml
|
||||
<Import Project="addons/forge/Forge.props" />
|
||||
```
|
||||
3. Back in the Godot editor, build your project by clicking `Build` in the top-right corner of the script editor.
|
||||
4. Enable **Forge Gameplay System** in `Project > Project Settings > Plugins`.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- See the [Quick Start Guide](https://github.com/gamesmiths-guild/forge-godot/blob/main/docs/quick-start.md) for a basic setup.
|
||||
- Explore [sample scenes](https://github.com/gamesmiths-guild/forge-godot/tree/main/examples) by cloning the full repo.
|
||||
|
||||
## 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
|
||||
|
||||
This plugin is licensed under the same terms as the core [Forge Gameplay System](https://github.com/gamesmiths-guild/forge).
|
||||
312
addons/forge/core/EffectApplier.cs
Normal file
312
addons/forge/core/EffectApplier.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Effects;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
internal sealed class EffectApplier
|
||||
{
|
||||
private record struct EffectKey(EffectData EffectData, EffectOwnership EffectOwnership, int Level);
|
||||
|
||||
private readonly List<EffectData> _effects = [];
|
||||
|
||||
private readonly Dictionary<IForgeEntity, List<ActiveEffectHandle>> _effectInstances = [];
|
||||
private readonly Dictionary<EffectKey, Effect> _effectsCache = [];
|
||||
|
||||
public EffectApplier(Node node)
|
||||
{
|
||||
foreach (Node child in node.GetChildren())
|
||||
{
|
||||
if (child is ForgeEffect effectNode && effectNode.EffectData is not null)
|
||||
{
|
||||
_effects.Add(effectNode.EffectData.GetEffectData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyEffects(
|
||||
Node node,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level = 1)
|
||||
{
|
||||
if (node is IForgeEntity forgeEntity)
|
||||
{
|
||||
ApplyEffects(forgeEntity, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node? child in node.GetChildren())
|
||||
{
|
||||
if (child is IForgeEntity forgeEntityChild)
|
||||
{
|
||||
ApplyEffects(forgeEntityChild, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyEffects<TData>(
|
||||
Node node,
|
||||
TData contextData,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level = 1)
|
||||
{
|
||||
if (node is IForgeEntity forgeEntity)
|
||||
{
|
||||
ApplyEffects(forgeEntity, contextData, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node? child in node.GetChildren())
|
||||
{
|
||||
if (child is IForgeEntity forgeEntityChild)
|
||||
{
|
||||
ApplyEffects(forgeEntityChild, contextData, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEffects(
|
||||
Node node,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
if (node is IForgeEntity forgeEntity)
|
||||
{
|
||||
AddEffects(forgeEntity, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node? child in node.GetChildren())
|
||||
{
|
||||
if (child is IForgeEntity forgeEntityChild)
|
||||
{
|
||||
AddEffects(forgeEntityChild, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEffects<TData>(
|
||||
Node node,
|
||||
TData contextData,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
if (node is IForgeEntity forgeEntity)
|
||||
{
|
||||
AddEffects(forgeEntity, contextData, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node? child in node.GetChildren())
|
||||
{
|
||||
if (child is IForgeEntity forgeEntityChild)
|
||||
{
|
||||
AddEffects(forgeEntityChild, contextData, effectOwner, effectSource, level);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEffects(Node node)
|
||||
{
|
||||
if (node is IForgeEntity forgeEntity)
|
||||
{
|
||||
RemoveEffects(forgeEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node? child in node.GetChildren())
|
||||
{
|
||||
if (child is IForgeEntity forgeEntityChild)
|
||||
{
|
||||
RemoveEffects(forgeEntityChild);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffects(
|
||||
IForgeEntity forgeEntity,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
var effectOwnership = new EffectOwnership(effectOwner, effectSource);
|
||||
|
||||
foreach (EffectData effectData in _effects)
|
||||
{
|
||||
var key = new EffectKey(effectData, effectOwnership, level);
|
||||
|
||||
if (_effectsCache.TryGetValue(key, out Effect? cachedEffect))
|
||||
{
|
||||
forgeEntity.EffectsManager.ApplyEffect(cachedEffect);
|
||||
continue;
|
||||
}
|
||||
|
||||
var effect = new Effect(
|
||||
effectData,
|
||||
new EffectOwnership(effectOwner, effectSource),
|
||||
level);
|
||||
|
||||
_effectsCache[key] = effect;
|
||||
|
||||
forgeEntity.EffectsManager.ApplyEffect(effect);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffects<TData>(
|
||||
IForgeEntity forgeEntity,
|
||||
TData contextData,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
var effectOwnership = new EffectOwnership(effectOwner, effectSource);
|
||||
|
||||
foreach (EffectData effectData in _effects)
|
||||
{
|
||||
var key = new EffectKey(effectData, effectOwnership, level);
|
||||
|
||||
if (_effectsCache.TryGetValue(key, out Effect? cachedEffect))
|
||||
{
|
||||
forgeEntity.EffectsManager.ApplyEffect(cachedEffect);
|
||||
continue;
|
||||
}
|
||||
|
||||
var effect = new Effect(
|
||||
effectData,
|
||||
new EffectOwnership(effectOwner, effectSource),
|
||||
level);
|
||||
|
||||
_effectsCache[key] = effect;
|
||||
|
||||
forgeEntity.EffectsManager.ApplyEffect(effect, contextData);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEffects(
|
||||
IForgeEntity forgeEntity,
|
||||
IForgeEntity?
|
||||
effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
var instanceEffects = new List<ActiveEffectHandle>();
|
||||
if (!_effectInstances.TryAdd(forgeEntity, instanceEffects))
|
||||
{
|
||||
instanceEffects = _effectInstances[forgeEntity];
|
||||
}
|
||||
|
||||
var effectOwnership = new EffectOwnership(effectOwner, effectSource);
|
||||
|
||||
foreach (EffectData effectData in _effects)
|
||||
{
|
||||
var key = new EffectKey(effectData, effectOwnership, level);
|
||||
|
||||
ActiveEffectHandle? handle;
|
||||
if (_effectsCache.TryGetValue(key, out Effect? cachedEffect))
|
||||
{
|
||||
handle = forgeEntity.EffectsManager.ApplyEffect(cachedEffect);
|
||||
|
||||
if (handle is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceEffects.Add(handle);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var effect = new Effect(
|
||||
effectData,
|
||||
new EffectOwnership(effectOwner, effectSource),
|
||||
level);
|
||||
|
||||
handle = forgeEntity.EffectsManager.ApplyEffect(effect);
|
||||
|
||||
if (handle is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceEffects.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEffects<TData>(
|
||||
IForgeEntity forgeEntity,
|
||||
TData contextData,
|
||||
IForgeEntity? effectOwner,
|
||||
IForgeEntity? effectSource,
|
||||
int level)
|
||||
{
|
||||
var instanceEffects = new List<ActiveEffectHandle>();
|
||||
if (!_effectInstances.TryAdd(forgeEntity, instanceEffects))
|
||||
{
|
||||
instanceEffects = _effectInstances[forgeEntity];
|
||||
}
|
||||
|
||||
var effectOwnership = new EffectOwnership(effectOwner, effectSource);
|
||||
|
||||
foreach (EffectData effectData in _effects)
|
||||
{
|
||||
var key = new EffectKey(effectData, effectOwnership, level);
|
||||
|
||||
ActiveEffectHandle? handle;
|
||||
if (_effectsCache.TryGetValue(key, out Effect? cachedEffect))
|
||||
{
|
||||
handle = forgeEntity.EffectsManager.ApplyEffect(cachedEffect);
|
||||
|
||||
if (handle is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceEffects.Add(handle);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var effect = new Effect(
|
||||
effectData,
|
||||
new EffectOwnership(effectOwner, effectSource),
|
||||
level);
|
||||
|
||||
handle = forgeEntity.EffectsManager.ApplyEffect(effect, contextData);
|
||||
|
||||
if (handle is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceEffects.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEffects(IForgeEntity forgeEntity)
|
||||
{
|
||||
if (!_effectInstances.TryGetValue(forgeEntity, out List<ActiveEffectHandle>? value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ActiveEffectHandle handle in value)
|
||||
{
|
||||
forgeEntity.EffectsManager.RemoveEffect(handle);
|
||||
}
|
||||
|
||||
_effectInstances[forgeEntity] = [];
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/EffectApplier.cs.uid
Normal file
1
addons/forge/core/EffectApplier.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bs52uo5esaiu2
|
||||
14
addons/forge/core/ForgeBootstrap.cs
Normal file
14
addons/forge/core/ForgeBootstrap.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
public partial class ForgeBootstrap : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
_ = new ForgeManagers(pluginData);
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/ForgeBootstrap.cs.uid
Normal file
1
addons/forge/core/ForgeBootstrap.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ba8fquhtwu5mu
|
||||
21
addons/forge/core/ForgeCurve.cs
Normal file
21
addons/forge/core/ForgeCurve.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
public readonly struct ForgeCurve(Curve? curve) : ICurve
|
||||
{
|
||||
private readonly Curve? _curve = curve;
|
||||
|
||||
public float Evaluate(float value)
|
||||
{
|
||||
if (_curve is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return _curve.Sample(value);
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/ForgeCurve.cs.uid
Normal file
1
addons/forge/core/ForgeCurve.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cei2cgvy84iy6
|
||||
15
addons/forge/core/ForgeData.cs
Normal file
15
addons/forge/core/ForgeData.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
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; } = [];
|
||||
}
|
||||
1
addons/forge/core/ForgeData.cs.uid
Normal file
1
addons/forge/core/ForgeData.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bq4vlbfx00hea
|
||||
30
addons/forge/core/ForgeManagers.cs
Normal file
30
addons/forge/core/ForgeManagers.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Gamesmiths.Forge.Cues;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
public class ForgeManagers
|
||||
{
|
||||
public static ForgeManagers Instance { get; private set; } = null!;
|
||||
|
||||
public TagsManager TagsManager { get; private set; }
|
||||
|
||||
public CuesManager CuesManager { get; private set; }
|
||||
|
||||
public ForgeManagers(ForgeData pluginData)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
#if DEBUG
|
||||
Validation.Enabled = true;
|
||||
#else
|
||||
Validation.Enabled = false;
|
||||
#endif
|
||||
|
||||
TagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
CuesManager = new CuesManager();
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/ForgeManagers.cs.uid
Normal file
1
addons/forge/core/ForgeManagers.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://djeiinm1gclh4
|
||||
98
addons/forge/core/ForgeRandom.cs
Normal file
98
addons/forge/core/ForgeRandom.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using System;
|
||||
using Gamesmiths.Forge.Core;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Core;
|
||||
|
||||
public class ForgeRandom : IRandom, IDisposable
|
||||
{
|
||||
private readonly RandomNumberGenerator _randomNumberGenerator;
|
||||
|
||||
public ForgeRandom()
|
||||
{
|
||||
_randomNumberGenerator = new RandomNumberGenerator();
|
||||
_randomNumberGenerator.Randomize();
|
||||
}
|
||||
|
||||
public void NextBytes(byte[] buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
buffer[i] = (byte)_randomNumberGenerator.RandiRange(0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
public void NextBytes(Span<byte> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
buffer[i] = (byte)_randomNumberGenerator.RandiRange(0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
public double NextDouble()
|
||||
{
|
||||
return _randomNumberGenerator.Randf();
|
||||
}
|
||||
|
||||
public int NextInt()
|
||||
{
|
||||
return (int)_randomNumberGenerator.Randi();
|
||||
}
|
||||
|
||||
public int NextInt(int maxValue)
|
||||
{
|
||||
return _randomNumberGenerator.RandiRange(0, maxValue - 1);
|
||||
}
|
||||
|
||||
public int NextInt(int minValue, int maxValue)
|
||||
{
|
||||
return _randomNumberGenerator.RandiRange(minValue, maxValue - 1);
|
||||
}
|
||||
|
||||
public long NextInt64()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var high = _randomNumberGenerator.Randi();
|
||||
var low = _randomNumberGenerator.Randi();
|
||||
return ((long)high << 32) | low;
|
||||
}
|
||||
}
|
||||
|
||||
public long NextInt64(long maxValue)
|
||||
{
|
||||
return NextInt64(0, maxValue);
|
||||
}
|
||||
|
||||
public long NextInt64(long minValue, long maxValue)
|
||||
{
|
||||
if (minValue >= maxValue)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minValue), "minValue must be less than maxValue.");
|
||||
}
|
||||
|
||||
var range = (ulong)(maxValue - minValue);
|
||||
var rand = (ulong)NextInt64();
|
||||
|
||||
return (long)(rand % range) + minValue;
|
||||
}
|
||||
|
||||
public float NextSingle()
|
||||
{
|
||||
return _randomNumberGenerator.Randf();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
_randomNumberGenerator.Dispose();
|
||||
}
|
||||
}
|
||||
1
addons/forge/core/ForgeRandom.cs.uid
Normal file
1
addons/forge/core/ForgeRandom.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dap6x2ddf6baj
|
||||
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
|
||||
7
addons/forge/core/forge_data.tres
Normal file
7
addons/forge/core/forge_data.tres
Normal file
@@ -0,0 +1,7 @@
|
||||
[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", "test"])
|
||||
210
addons/forge/editor/AssetRepairTool.cs
Normal file
210
addons/forge/editor/AssetRepairTool.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor;
|
||||
|
||||
[Tool]
|
||||
public partial class AssetRepairTool : EditorPlugin
|
||||
{
|
||||
public static void RepairAllAssetsTags()
|
||||
{
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
|
||||
List<string> scenes = GetScenePaths("res://");
|
||||
GD.Print($"Found {scenes.Count} scene(s) to process.");
|
||||
|
||||
var openedScenes = EditorInterface.Singleton.GetOpenScenes();
|
||||
|
||||
foreach (var originalScenePath in scenes)
|
||||
{
|
||||
// For some weird reason scenes from the GetScenePath are coming with 3 slashes instead of just two.
|
||||
var scenePath = originalScenePath.Replace("res:///", "res://");
|
||||
|
||||
GD.Print($"Processing scene: {scenePath}.");
|
||||
PackedScene? packedScene = ResourceLoader.Load<PackedScene>(scenePath);
|
||||
|
||||
if (packedScene is null)
|
||||
{
|
||||
GD.PrintErr($"Failed to load scene: {scenePath}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Node sceneInstance = packedScene.Instantiate();
|
||||
var modified = ProcessNode(sceneInstance, tagsManager);
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
GD.Print($"No changes needed for {scenePath}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 'sceneInstance' is the modified scene instance in memory, need to save to disk and reload if needed.
|
||||
var newScene = new PackedScene();
|
||||
Error error = newScene.Pack(sceneInstance);
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
GD.PrintErr($"Failed to pack scene: {error}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
error = ResourceSaver.Save(newScene, scenePath);
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
GD.PrintErr($"Failed to save scene: {error}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (openedScenes.Contains(scenePath))
|
||||
{
|
||||
GD.Print($"Scene was opened, reloading background scene: {scenePath}.");
|
||||
EditorInterface.Singleton.ReloadSceneFromPath(scenePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively get scene files from a folder.
|
||||
/// </summary>
|
||||
/// <param name="basePath">Current path iteration.</param>
|
||||
/// <returns>List of scenes found.</returns>
|
||||
private static List<string> GetScenePaths(string basePath)
|
||||
{
|
||||
var scenePaths = new List<string>();
|
||||
var dir = DirAccess.Open(basePath);
|
||||
|
||||
if (dir is null)
|
||||
{
|
||||
GD.PrintErr($"Failed to open directory: {basePath}");
|
||||
return scenePaths;
|
||||
}
|
||||
|
||||
// Start listing directory entries; skip navigational and hidden files.
|
||||
dir.ListDirBegin();
|
||||
while (true)
|
||||
{
|
||||
var fileName = dir.GetNext();
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var filePath = $"{basePath}/{fileName}";
|
||||
if (dir.CurrentIsDir())
|
||||
{
|
||||
// Recursively scan subdirectories.
|
||||
scenePaths.AddRange(GetScenePaths(filePath));
|
||||
}
|
||||
else if (fileName.EndsWith(".tscn", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fileName.EndsWith(".scn", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
scenePaths.Add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
dir.ListDirEnd();
|
||||
return scenePaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively process nodes; returns true if any ForgeEntity was modified.
|
||||
/// </summary>
|
||||
/// <param name="node">Current node iteration.</param>
|
||||
/// <param name="tagsManager">The tags manager used to validate tags.</param>
|
||||
/// <returns><see langword="true"/> if any ForgeEntity was modified.</returns>
|
||||
private static bool ProcessNode(Node node, TagsManager tagsManager)
|
||||
{
|
||||
var modified = ValidateNode(node, tagsManager);
|
||||
|
||||
foreach (Node child in node.GetChildren())
|
||||
{
|
||||
modified |= ProcessNode(child, tagsManager);
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
private static bool ValidateNode(Node node, TagsManager tagsManager)
|
||||
{
|
||||
var modified = false;
|
||||
foreach (Dictionary propertyInfo in node.GetPropertyList())
|
||||
{
|
||||
if (!propertyInfo.TryGetValue("class_name", out Variant className))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (className.AsString() != "TagContainer")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propertyInfo.TryGetValue("name", out Variant nameObj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyName = nameObj.AsString();
|
||||
Variant value = node.Get(propertyName);
|
||||
|
||||
if (value.VariantType != Variant.Type.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.As<Resource>() is ForgeTagContainer tagContainer)
|
||||
{
|
||||
modified |= ValidateTagContainerProperty(tagContainer, node.Name, tagsManager);
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
private static bool ValidateTagContainerProperty(
|
||||
ForgeTagContainer container,
|
||||
string nodeName,
|
||||
TagsManager tagsManager)
|
||||
{
|
||||
if (container.ContainerTags is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Array<string> originalTags = container.ContainerTags;
|
||||
var newTags = new Array<string>();
|
||||
var modified = false;
|
||||
|
||||
foreach (var tag in originalTags)
|
||||
{
|
||||
try
|
||||
{
|
||||
Tag.RequestTag(tagsManager, tag);
|
||||
newTags.Add(tag);
|
||||
}
|
||||
catch (TagNotRegisteredException)
|
||||
{
|
||||
GD.PrintRich(
|
||||
$"[color=LIGHT_STEEL_BLUE][RepairTool] Removing invalid tag [{tag}] from node {nodeName}.");
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modified)
|
||||
{
|
||||
container.ContainerTags = newTags;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/AssetRepairTool.cs.uid
Normal file
1
addons/forge/editor/AssetRepairTool.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://1runivyr5don
|
||||
59
addons/forge/editor/EditorUtils.cs
Normal file
59
addons/forge/editor/EditorUtils.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor;
|
||||
|
||||
internal static class EditorUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses reflection to gather all classes inheriting from AttributeSet and their fields of type Attribute.
|
||||
/// </summary>
|
||||
/// <returns>An array with the available attributes.</returns>
|
||||
public static string[] GetAttributeSetOptions()
|
||||
{
|
||||
var options = new List<string>();
|
||||
|
||||
foreach (Type attributeSetType in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(x => x.IsSubclassOf(typeof(AttributeSet))))
|
||||
{
|
||||
options.Add(attributeSetType.Name);
|
||||
}
|
||||
|
||||
return [.. options];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses reflection to gather all classes inheriting from AttributeSet and their fields of type Attribute.
|
||||
/// </summary>
|
||||
/// <param name="attributeSet">The attribute set used to search for the attributes.</param>
|
||||
/// <returns>An array with the available attributes.</returns>
|
||||
public static string[] GetAttributeOptions(string? attributeSet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(attributeSet))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
Type? type = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(x => x.IsSubclassOf(typeof(AttributeSet)) && x.Name == attributeSet);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
IEnumerable<PropertyInfo> properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
return [.. properties.Select(x => $"{x.Name}")];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/EditorUtils.cs.uid
Normal file
1
addons/forge/editor/EditorUtils.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dcvmf0r1f43m6
|
||||
14
addons/forge/editor/ForgeEditor.tscn
Normal file
14
addons/forge/editor/ForgeEditor.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene format=3 uid="uid://pjscvogl6jak"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c17f812by5x23" path="res://addons/forge/editor/tags/TagsEditor.tscn" id="1_bxwfw"]
|
||||
|
||||
[node name="Forge" type="PanelContainer" unique_id=249446352]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Tags" parent="." unique_id=654228508 instance=ExtResource("1_bxwfw")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
34
addons/forge/editor/attributes/AttributeEditorPlugin.cs
Normal file
34
addons/forge/editor/attributes/AttributeEditorPlugin.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeEditorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is Resources.ForgeModifier || @object is Resources.ForgeCue;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "Attribute" || name == "CapturedAttribute" || name == "MagnitudeAttribute")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl2w0vp6b5p8k
|
||||
113
addons/forge/editor/attributes/AttributeEditorProperty.cs
Normal file
113
addons/forge/editor/attributes/AttributeEditorProperty.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private const int ButtonSize = 26;
|
||||
private const int PopupSize = 300;
|
||||
|
||||
private Label _label = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("GuiDropdown", "EditorIcons");
|
||||
|
||||
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);
|
||||
|
||||
var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) };
|
||||
var tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
AnchorRight = 1,
|
||||
AnchorBottom = 1,
|
||||
};
|
||||
popup.AddChild(tree);
|
||||
|
||||
var bg = new StyleBoxFlat
|
||||
{
|
||||
BgColor = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetColor("dark_color_2", "Editor"),
|
||||
};
|
||||
tree.AddThemeStyleboxOverride("panel", bg);
|
||||
|
||||
AddChild(popup);
|
||||
|
||||
BuildAttributeTree(tree);
|
||||
|
||||
button.Pressed += () =>
|
||||
{
|
||||
Window win = GetWindow();
|
||||
popup.Position = (Vector2I)button.GlobalPosition
|
||||
+ win.Position
|
||||
- new Vector2I(PopupSize - ButtonSize, -30);
|
||||
popup.Popup();
|
||||
};
|
||||
|
||||
tree.ItemActivated += () =>
|
||||
{
|
||||
TreeItem item = tree.GetSelected();
|
||||
if (item?.HasMeta("attribute_path") != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fullPath = item.GetMeta("attribute_path").AsString();
|
||||
_label.Text = fullPath;
|
||||
EmitChanged(GetEditedProperty(), fullPath);
|
||||
popup.Hide();
|
||||
};
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
var value = GetEditedObject().Get(GetEditedProperty()).AsString();
|
||||
_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();
|
||||
|
||||
foreach (var attributeSet in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
TreeItem setItem = tree.CreateItem(root);
|
||||
setItem.SetText(0, attributeSet);
|
||||
setItem.Collapsed = true;
|
||||
|
||||
foreach (var attribute in EditorUtils.GetAttributeOptions(attributeSet))
|
||||
{
|
||||
TreeItem attributeItem = tree.CreateItem(setItem);
|
||||
var attributePath = $"{attributeSet}.{attribute}";
|
||||
attributeItem.SetText(0, attribute);
|
||||
attributeItem.SetMeta("attribute_path", attributePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvjqj637kfav
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetClassEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
private OptionButton _optionButton = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_optionButton = new OptionButton();
|
||||
AddChild(_optionButton);
|
||||
|
||||
_optionButton.AddItem("Select AttributeSet Class");
|
||||
foreach (var option in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
_optionButton.AddItem(option);
|
||||
}
|
||||
|
||||
_optionButton.ItemSelected += x =>
|
||||
{
|
||||
var className = _optionButton.GetItemText((int)x);
|
||||
EmitChanged(GetEditedProperty(), className);
|
||||
|
||||
GodotObject @object = GetEditedObject();
|
||||
if (@object is not null)
|
||||
{
|
||||
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> attributeProperties = targetType
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
foreach (var propertyName in attributeProperties.Select(x => x.Name))
|
||||
{
|
||||
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", dictionary);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
GodotObject obj = GetEditedObject();
|
||||
StringName property = GetEditedProperty();
|
||||
var val = obj.Get(property).AsString();
|
||||
for (var i = 0; i < _optionButton.GetItemCount(); i++)
|
||||
{
|
||||
if (_optionButton.GetItemText(i) == val)
|
||||
{
|
||||
_optionButton.Selected = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
for (var i = GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
Node child = GetChild(i);
|
||||
RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bumuxlivyt66b
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
private PackedScene? _inspectorScene;
|
||||
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
return @object is ForgeAttributeSet;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "AttributeSetClass")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeSetClassEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == "InitialAttributeValues")
|
||||
{
|
||||
AddPropertyEditor(name, new AttributeSetValuesEditorProperty());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://t3gpjlcyqor
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Gamesmiths.Forge.Attributes;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeSetValuesEditorProperty : EditorProperty, ISerializationListener
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var attributesRoot = new VBoxContainer { Name = "AttributesRoot" };
|
||||
AddChild(attributesRoot);
|
||||
SetBottomEditor(attributesRoot);
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
VBoxContainer attributesRoot = GetNodeOrNull<VBoxContainer>("AttributesRoot");
|
||||
|
||||
if (attributesRoot is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FreeAllChildren(attributesRoot);
|
||||
|
||||
if (GetEditedObject() is not ForgeAttributeSet obj
|
||||
|| string.IsNullOrEmpty(obj.AttributeSetClass)
|
||||
|| obj.InitialAttributeValues is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var className = obj.AttributeSetClass;
|
||||
var assembly = Assembly.GetAssembly(typeof(ForgeAttributeSet));
|
||||
Type? targetType = System.Array.Find(assembly?.GetTypes() ?? [], x => x.Name == className);
|
||||
|
||||
if (targetType is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Collections.Generic.IEnumerable<PropertyInfo> attributeProperties = targetType
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => x.PropertyType == typeof(EntityAttribute));
|
||||
|
||||
foreach (var attributeName in attributeProperties.Select(x => x.Name))
|
||||
{
|
||||
var groupVBox = new VBoxContainer();
|
||||
|
||||
groupVBox.AddChild(AttributeHeader(attributeName));
|
||||
|
||||
AttributeValues value = obj.InitialAttributeValues.TryGetValue(attributeName, out AttributeValues? v)
|
||||
? v
|
||||
: new AttributeValues(0, 0, int.MaxValue);
|
||||
|
||||
SpinBox spinDefault = CreateSpinBox(value.Min, value.Max, value.Default);
|
||||
SpinBox spinMin = CreateSpinBox(int.MinValue, value.Max, value.Min);
|
||||
SpinBox spinMax = CreateSpinBox(value.Min, int.MaxValue, value.Max);
|
||||
|
||||
groupVBox.AddChild(AttributeFieldRow("Default", spinDefault));
|
||||
groupVBox.AddChild(AttributeFieldRow("Min", spinMin));
|
||||
groupVBox.AddChild(AttributeFieldRow("Max", spinMax));
|
||||
|
||||
spinDefault.ValueChanged += x =>
|
||||
{
|
||||
UpdateAndEmit(obj, attributeName, (int)x, (int)spinMin.Value, (int)spinMax.Value);
|
||||
};
|
||||
|
||||
spinMin.ValueChanged += x =>
|
||||
{
|
||||
spinDefault.MinValue = x;
|
||||
spinMax.MinValue = x;
|
||||
UpdateAndEmit(obj, attributeName, (int)spinDefault.Value, (int)x, (int)spinMax.Value);
|
||||
};
|
||||
|
||||
spinMax.ValueChanged += x =>
|
||||
{
|
||||
spinDefault.MaxValue = x;
|
||||
spinMin.MaxValue = x;
|
||||
UpdateAndEmit(obj, attributeName, (int)spinDefault.Value, (int)spinMin.Value, (int)x);
|
||||
};
|
||||
|
||||
attributesRoot.AddChild(groupVBox);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
CustomMinimumSize = new Vector2(0, 28),
|
||||
};
|
||||
|
||||
var style = new StyleBoxFlat
|
||||
{
|
||||
BgColor = new Color(0.16f, 0.17f, 0.20f),
|
||||
};
|
||||
|
||||
headerPanel.AddThemeStyleboxOverride("panel", style);
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = text,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
SizeFlagsHorizontal = (SizeFlags)(int)SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 22),
|
||||
AutowrapMode = TextServer.AutowrapMode.Off,
|
||||
};
|
||||
|
||||
headerPanel.AddChild(label);
|
||||
return headerPanel;
|
||||
}
|
||||
|
||||
private static HBoxContainer AttributeFieldRow(string label, SpinBox spinBox)
|
||||
{
|
||||
var hBox = new HBoxContainer();
|
||||
|
||||
hBox.AddChild(new Label
|
||||
{
|
||||
Text = label,
|
||||
CustomMinimumSize = new Vector2(80, 0),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
hBox.AddChild(spinBox);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
private static SpinBox CreateSpinBox(int min, int max, int value)
|
||||
{
|
||||
return new SpinBox
|
||||
{
|
||||
MinValue = min,
|
||||
MaxValue = max,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SelectAllOnFocus = true,
|
||||
Value = value,
|
||||
};
|
||||
}
|
||||
|
||||
private static void FreeAllChildren(Node node)
|
||||
{
|
||||
for (var i = node.GetChildCount() - 1; i >= 0; i--)
|
||||
{
|
||||
node.GetChild(i).QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAndEmit(ForgeAttributeSet obj, string name, int def, int min, int max)
|
||||
{
|
||||
Debug.Assert(obj.InitialAttributeValues is not null, "InitialAttributeValues should not be null here.");
|
||||
|
||||
var dict = new Dictionary<string, AttributeValues>(obj.InitialAttributeValues)
|
||||
{
|
||||
[name] = new AttributeValues(def, min, max),
|
||||
};
|
||||
|
||||
EmitChanged(nameof(ForgeAttributeSet.InitialAttributeValues), dict);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdj20gbpxkda1
|
||||
29
addons/forge/editor/attributes/AttributeValues.cs
Normal file
29
addons/forge/editor/attributes/AttributeValues.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Attributes;
|
||||
|
||||
[Tool]
|
||||
public partial class AttributeValues : RefCounted
|
||||
{
|
||||
[Export]
|
||||
public int Default { get; set; }
|
||||
|
||||
[Export]
|
||||
public int Min { get; set; }
|
||||
|
||||
[Export]
|
||||
public int Max { get; set; }
|
||||
|
||||
public AttributeValues()
|
||||
{
|
||||
}
|
||||
|
||||
public AttributeValues(int @default, int min, int max)
|
||||
{
|
||||
Default = @default;
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
1
addons/forge/editor/attributes/AttributeValues.cs.uid
Normal file
1
addons/forge/editor/attributes/AttributeValues.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccovd5i0wr3kk
|
||||
53
addons/forge/editor/cues/CueHandlerInspectorPlugin.cs
Normal file
53
addons/forge/editor/cues/CueHandlerInspectorPlugin.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Nodes;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Cues;
|
||||
|
||||
[Tool]
|
||||
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.
|
||||
if (@object?.GetScript().As<CSharpScript>() is CSharpScript script)
|
||||
{
|
||||
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(
|
||||
GodotObject @object,
|
||||
Variant.Type type,
|
||||
string name,
|
||||
PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags,
|
||||
bool wide)
|
||||
{
|
||||
if (name == "CueTag")
|
||||
{
|
||||
var cueKeyEditorProperty = new CueKeyEditorProperty();
|
||||
AddPropertyEditor(name, cueKeyEditorProperty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base._ParseProperty(@object, type, name, hintType, hintString, usageFlags, wide);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dattkelp87mhv
|
||||
106
addons/forge/editor/cues/CueKeyEditorProperty.cs
Normal file
106
addons/forge/editor/cues/CueKeyEditorProperty.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Cues;
|
||||
|
||||
[Tool]
|
||||
public partial class CueKeyEditorProperty : EditorProperty
|
||||
{
|
||||
private const int ButtonSize = 26;
|
||||
private const int PopupSize = 300;
|
||||
|
||||
private Label _label = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Texture2D dropdownIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiDropdown", "EditorIcons");
|
||||
|
||||
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);
|
||||
|
||||
var popup = new Popup { Size = new Vector2I(PopupSize, PopupSize) };
|
||||
var tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
AnchorRight = 1,
|
||||
AnchorBottom = 1,
|
||||
};
|
||||
popup.AddChild(tree);
|
||||
|
||||
var backgroundStyle = new StyleBoxFlat
|
||||
{
|
||||
BgColor = EditorInterface.Singleton.GetEditorTheme().GetColor("base_color", "Editor"),
|
||||
};
|
||||
tree.AddThemeStyleboxOverride("panel", backgroundStyle);
|
||||
|
||||
AddChild(popup);
|
||||
|
||||
ForgeData pluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. pluginData.RegisteredTags]);
|
||||
TreeItem root = tree.CreateItem();
|
||||
BuildTreeRecursively(tree, root, tagsManager.RootNode);
|
||||
|
||||
button.Pressed += () =>
|
||||
{
|
||||
Window win = GetWindow();
|
||||
popup.Position = (Vector2I)button.GlobalPosition
|
||||
+ win.Position
|
||||
- new Vector2I(PopupSize - ButtonSize, -30);
|
||||
popup.Popup();
|
||||
};
|
||||
|
||||
tree.ItemActivated += () =>
|
||||
{
|
||||
TreeItem item = tree.GetSelected();
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Build full path from root.
|
||||
var segments = new List<string>();
|
||||
TreeItem current = item;
|
||||
while (current.GetParent() is not null)
|
||||
{
|
||||
segments.Insert(0, current.GetText(0));
|
||||
current = current.GetParent();
|
||||
}
|
||||
|
||||
var fullPath = string.Join(".", segments);
|
||||
|
||||
_label.Text = fullPath;
|
||||
EmitChanged(GetEditedProperty(), fullPath);
|
||||
popup.Hide();
|
||||
};
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
var property = GetEditedObject().Get(GetEditedProperty()).AsString();
|
||||
_label.Text = string.IsNullOrEmpty(property) ? "None" : property;
|
||||
}
|
||||
|
||||
private static 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.Collapsed = true;
|
||||
BuildTreeRecursively(tree, childTreeNode, childTagNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
addons/forge/editor/cues/CueKeyEditorProperty.cs.uid
Normal file
1
addons/forge/editor/cues/CueKeyEditorProperty.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://csmr2puffid4k
|
||||
1
addons/forge/editor/cues/CueKeysEditor.cs.uid
Normal file
1
addons/forge/editor/cues/CueKeysEditor.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnsy7p8h1ujjv
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user