Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .claude/hooks/rebuild-analyzers-on-change.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# PostToolUse hook: rebuild the Roslyn analyzer after edits inside the analyzer
# project (a git submodule), then redeploy the DLL into the Unity package.
# The submodule has no Directory.Build.targets on purpose (it stays independent
# of this repo's layout), so the copy step lives here.
#
# Path-scoped on purpose:
# - Triggers ONLY for *.cs under Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/
# - Skips the Tests and Sample projects.
#
# Build success -> exit 0 (silent).
# Path mismatch -> exit 0 (silent).
# Build failure -> exit 2 with stderr piped through, so the assistant sees it.

set -uo pipefail

file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null)

case "$file_path" in
*/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/*.cs) ;;
*) exit 0 ;;
esac

cd "$CLAUDE_PROJECT_DIR" || exit 0

dotnet build \
Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.csproj \
-c Release --nologo -v quiet 1>&2 || exit 2

cp Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/bin/Release/netstandard2.0/Aspid.FastTools.Analyzers.dll \
Aspid.FastTools/Packages/tech.aspid.fasttools/Aspid.FastTools.Analyzers.dll 1>&2 || exit 2
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rebuild-generators-on-change.sh\""
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rebuild-analyzers-on-change.sh\""
}
]
}
Expand Down
17 changes: 17 additions & 0 deletions .claude/skills/build-analyzer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: build-analyzer
description: Build the Roslyn analyzer submodule and deploy the resulting DLL into the Unity package
user-invocable: true
---

Build the Aspid.FastTools analyzer (git submodule) and deploy to Unity:

1. If `Aspid.FastTools.Analyzers/` is empty, run `git submodule update --init` first
2. Run `dotnet build Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.csproj -c Release` from the repository root
3. Run `dotnet test Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.sln -c Release` and stop if any test fails
4. Copy `Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/bin/Release/netstandard2.0/Aspid.FastTools.Analyzers.dll` to `Aspid.FastTools/Packages/tech.aspid.fasttools/Aspid.FastTools.Analyzers.dll`
5. Report the result: build/test output, any errors, and confirm the DLL was copied successfully

Note: diagnostic IDs use the `AFT*` prefix. After bumping the submodule commit, remember the gitlink change in the superproject (`git add Aspid.FastTools.Analyzers`).

Arguments: $ARGUMENTS (optional: pass `Debug` to build in Debug configuration instead of Release)
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "Aspid.FastTools.Analyzers"]
path = Aspid.FastTools.Analyzers
url = https://github.com/VPDPersonal/Aspid.FastTools.Analyzers.git
1 change: 1 addition & 0 deletions Aspid.FastTools.Analyzers
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,10 @@ public sealed class TankEnemy : EnemyBase

## SerializeReference Selector

A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSelector]` next to `[SerializeReference]` and the Inspector replaces the default managed-reference UI with the same searchable, hierarchical type picker used by `SerializableType` — letting you choose which concrete implementation of the field's declared type is instantiated.
A drop-in dropdown for `[SerializeReference]` fields. Add `[TypeSelector]` next to `[SerializeReference]` and the Inspector replaces the default managed-reference UI with the same searchable, hierarchical type picker used by `SerializableType` — letting you choose which concrete implementation of the field's declared type is instantiated.

- Lists every concrete, non-`UnityEngine.Object` class assignable to the field's declared interface / base type.
- Passing base types narrows the candidates below the field's declared type — `[TypeSelector(typeof(IMelee))]` on an `IWeapon` field offers only `IMelee` implementations.
- Picking a type instantiates it; `<None>` clears the reference.
- The assigned instance's serialized fields are drawn inline under a foldout.
- A stored type that no longer resolves (renamed or deleted) is surfaced as a missing-type warning instead of silently clearing.
Expand All @@ -348,7 +349,7 @@ A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSe
using System;
using UnityEngine;
using System.Collections.Generic;
using Aspid.FastTools.SerializeReferences;
using Aspid.FastTools.Types;

public interface IWeapon
{
Expand All @@ -373,10 +374,10 @@ public sealed class Railgun : IWeapon

public sealed class Loadout : MonoBehaviour
{
[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private IWeapon _primary;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private List<IWeapon> _sidearms;
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,10 @@ public sealed class TankEnemy : EnemyBase

## SerializeReference Selector

Готовый выпадающий список для полей с `[SerializeReference]`. Добавьте `[SerializeReferenceSelector]` рядом с `[SerializeReference]`, и Inspector заменит стандартный UI managed-ссылки тем же иерархическим выбором типа с поиском, что используется в `SerializableType` — позволяя выбрать, какая конкретная реализация объявленного типа поля будет создана.
Готовый выпадающий список для полей с `[SerializeReference]`. Добавьте `[TypeSelector]` рядом с `[SerializeReference]`, и Inspector заменит стандартный UI managed-ссылки тем же иерархическим выбором типа с поиском, что используется в `SerializableType` — позволяя выбрать, какая конкретная реализация объявленного типа поля будет создана.

- Показывает каждый конкретный, не наследующий `UnityEngine.Object` класс, совместимый с объявленным интерфейсом / базовым типом поля.
- Передача базовых типов сужает список ниже объявленного типа поля — `[TypeSelector(typeof(IMelee))]` на поле `IWeapon` предлагает только реализации `IMelee`.
- Выбор типа создаёт его экземпляр; `<None>` очищает ссылку.
- Сериализуемые поля назначенного экземпляра рисуются вложенно под foldout.
- Сохранённый тип, который больше не разрешается (переименован или удалён), показывается как предупреждение о потерянном типе, а не очищается молча.
Expand All @@ -348,7 +349,7 @@ public sealed class TankEnemy : EnemyBase
using System;
using UnityEngine;
using System.Collections.Generic;
using Aspid.FastTools.SerializeReferences;
using Aspid.FastTools.Types;

public interface IWeapon
{
Expand All @@ -373,10 +374,10 @@ public sealed class Railgun : IWeapon

public sealed class Loadout : MonoBehaviour
{
[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private IWeapon _primary;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private List<IWeapon> _sidearms;
}
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# SerializeReferences Sample

A tiny loadout system that demonstrates `[SerializeReferenceSelector]` — a searchable, hierarchical type dropdown for `[SerializeReference]` fields. You pick which concrete implementation of a polymorphic field is instantiated, directly in the Inspector.
A tiny loadout system that demonstrates `[TypeSelector]` — a searchable, hierarchical type dropdown for `[SerializeReference]` fields. You pick which concrete implementation of a polymorphic field is instantiated, directly in the Inspector.

Look at:

- `Scripts/Loadout.cs` — single (`IWeapon`), `List<IWeapon>`, and abstract-base (`StatusEffect`) `[SerializeReference]` fields, each annotated with `[SerializeReferenceSelector]`.
- `Scripts/Weapons/` — `IWeapon` interface and its implementations (`Pistol`, `Shotgun`, `Railgun`). `Railgun` nests another `[SerializeReferenceSelector]` field, showing recursive polymorphic editing.
- `Scripts/Loadout.cs` — single (`IWeapon`), `List<IWeapon>`, and abstract-base (`StatusEffect`) `[SerializeReference]` fields, each annotated with `[TypeSelector]`.
- `Scripts/Weapons/` — `IWeapon` interface and its implementations (`Pistol`, `Shotgun`, `Railgun`). `Railgun` nests another `[TypeSelector]` field, showing recursive polymorphic editing.
- `Scripts/Effects/` — abstract `StatusEffect` base with `BurnEffect` / `FreezeEffect`. The dropdown offers only the concrete subclasses; the abstract base is never listed.
- `Scripts/Modifiers/` — generic hierarchy: a non-abstract `Modifier<T>` generic class (`IModifier`) with closed-generic subclasses `DamageModifier : Modifier<float>`, `AmmoModifier : Modifier<int>`, `NameModifier : Modifier<string>`. An `IModifier` field offers all three subclasses **and** the open generic `Modifier<T>` — picking `Modifier<T>` opens a second window to choose the argument `T`. A `Modifier<float>` field offers only the candidates assignable to it (`DamageModifier`, and `Modifier<T>` with `T` inferred to `float`).
- `Scripts/WeaponPreset.cs` + `Presets/BrokenWeaponPreset.asset` — a `ScriptableObject` whose `_weapon` points at a type that no longer exists, used to demonstrate the missing-type repair flow (see *Maintenance features* below).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Пример SerializeReferences

Маленькая система снаряжения, демонстрирующая `[SerializeReferenceSelector]` — иерархический выпадающий список с поиском для полей `[SerializeReference]`. Вы прямо в Inspector выбираете, какая конкретная реализация полиморфного поля будет создана.
Маленькая система снаряжения, демонстрирующая `[TypeSelector]` — иерархический выпадающий список с поиском для полей `[SerializeReference]`. Вы прямо в Inspector выбираете, какая конкретная реализация полиморфного поля будет создана.

Смотрите:

- `Scripts/Loadout.cs` — одиночное поле (`IWeapon`), `List<IWeapon>` и поле с абстрактным базовым типом (`StatusEffect`), каждое с `[SerializeReference]` и `[SerializeReferenceSelector]`.
- `Scripts/Weapons/` — интерфейс `IWeapon` и его реализации (`Pistol`, `Shotgun`, `Railgun`). `Railgun` вкладывает ещё одно поле `[SerializeReferenceSelector]` — показывает рекурсивное полиморфное редактирование.
- `Scripts/Loadout.cs` — одиночное поле (`IWeapon`), `List<IWeapon>` и поле с абстрактным базовым типом (`StatusEffect`), каждое с `[SerializeReference]` и `[TypeSelector]`.
- `Scripts/Weapons/` — интерфейс `IWeapon` и его реализации (`Pistol`, `Shotgun`, `Railgun`). `Railgun` вкладывает ещё одно поле `[TypeSelector]` — показывает рекурсивное полиморфное редактирование.
- `Scripts/Effects/` — абстрактный базовый `StatusEffect` с `BurnEffect` / `FreezeEffect`. В списке предлагаются только конкретные подтипы; абстрактный базовый класс никогда не показывается.
- `Scripts/Modifiers/` — generic-иерархия: неабстрактный generic-класс `Modifier<T>` (`IModifier`) с закрытыми подтипами `DamageModifier : Modifier<float>`, `AmmoModifier : Modifier<int>`, `NameModifier : Modifier<string>`. Поле `IModifier` предлагает все три подтипа **и** сам открытый `Modifier<T>` — при его выборе открывается второе окно для выбора аргумента `T`. Поле `Modifier<float>` предлагает только присваиваемых кандидатов (`DamageModifier` и `Modifier<T>` с выведенным `T = float`).
- `Scripts/WeaponPreset.cs` + `Presets/BrokenWeaponPreset.asset` — `ScriptableObject`, у которого поле `_weapon` указывает на несуществующий тип; используется для демонстрации починки потерянного типа (см. *Сервисные функции* ниже).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Aspid.FastTools.Samples.SerializeReferences.Editors
//
// Unity picks IMGUI vs UIToolkit at the Editor level: when CreateInspectorGUI is NOT
// overridden but OnInspectorGUI is, the whole inspector — including every nested
// PropertyDrawer — falls back to IMGUI. That routes [SerializeReferenceSelector] fields
// PropertyDrawer — falls back to IMGUI. That routes [TypeSelector] fields
// through SerializeReferenceIMGUIPropertyDrawer.OnGUI instead of CreatePropertyGUI.
[CustomEditor(typeof(IMGUILoadout))]
internal sealed class IMGUILoadoutEditor : Editor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Aspid.FastTools.Samples.SerializeReferences
{
// Abstract base for a second polymorphic hierarchy.
//
// When a field is declared as StatusEffect, [SerializeReferenceSelector] offers only the
// When a field is declared as StatusEffect, [TypeSelector] offers only the
// concrete subclasses (BurnEffect, FreezeEffect) — the abstract base itself is never listed,
// because it cannot be instantiated.
[Serializable]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
using System.Collections.Generic;
using UnityEngine;
using Aspid.FastTools.SerializeReferences;
using Aspid.FastTools.Types;

// ReSharper disable once CheckNamespace
namespace Aspid.FastTools.Samples.SerializeReferences
{
// Same fields as Loadout, but the companion editor (IMGUILoadoutEditor) overrides
// OnInspectorGUI without CreateInspectorGUI, forcing the entire inspector — and every
// nested [SerializeReferenceSelector] field — through the IMGUI path
// nested [TypeSelector] field — through the IMGUI path
// (SerializeReferenceIMGUIPropertyDrawer) instead of the UIToolkit one.
//
// Use this to verify both rendering paths stay visually and behaviourally aligned.
public sealed class IMGUILoadout : MonoBehaviour
{
[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private IWeapon _primaryWeapon;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private List<IWeapon> _sidearms = new();

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private StatusEffect _onHitEffect;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private IModifier _modifier;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private Modifier<float> _floatModifier;

[SerializeReference] [SerializeReferenceSelector]
[SerializeReference] [TypeSelector]
private List<IModifier> _modifiers = new();
}
}
Loading
Loading