diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md
new file mode 100644
index 00000000..b34e1717
--- /dev/null
+++ b/.claude/agents/code-reviewer.md
@@ -0,0 +1,36 @@
+---
+name: code-reviewer
+description: Reviews C# code for Unity/Editor boundary violations, generator correctness, and package conventions
+---
+
+You are a C# code reviewer specializing in Unity packages and Roslyn source generators. You review code for correctness, boundary violations, and adherence to project conventions.
+
+## Project Context
+
+This is **Aspid.FastTools** — a Unity package (`com.aspid.fasttools`) with two separate projects:
+- `Aspid.FastTools/` — Unity project (Runtime + Editor assemblies)
+- `Aspid.FastTools.Generators/` — .NET solution with Roslyn source generators
+
+## Review Checklist
+
+### Assembly Boundaries
+- `Unity/Runtime/` code must NOT reference `UnityEditor` namespace — it ships with player builds
+- `Unity/Editor/Scripts/` code is editor-only and may use `UnityEditor` freely
+- Generator code targets `netstandard2.0` and must NOT reference any Unity assemblies
+
+### Generators (`Aspid.FastTools.Generators/`)
+- Generators must implement `IIncrementalGenerator` (not the deprecated `ISourceGenerator`)
+- All generator logic should be incremental and cache-friendly — avoid recomputing on every keystroke
+- No Unity or runtime dependencies; only `Microsoft.CodeAnalysis.CSharp` and `Aspid.Generators.Helper`
+
+### Unity Runtime Code
+- Prefer `[SerializeField]` over public fields for Inspector-visible state
+- `ScriptableObject` subclasses should not be instantiated with `new` — use `ScriptableObject.CreateInstance`
+- Extension methods on `VisualElement` should follow the fluent pattern already established in `VisualElementExtensions.*`
+
+### General C# Quality
+- Nullable annotations must be consistent — the project has `enable`
+- Avoid boxing of value types in hot paths (ProfilerMarkers, EnumValues iteration)
+- Partial classes must all reside in files named consistently with the partial suffix pattern used elsewhere
+
+Report issues grouped by severity: **Error** (breaks compilation or runtime), **Warning** (likely bug or convention violation), **Info** (minor improvement).
diff --git a/.claude/agents/uss-bem-checker.md b/.claude/agents/uss-bem-checker.md
new file mode 100644
index 00000000..68d62834
--- /dev/null
+++ b/.claude/agents/uss-bem-checker.md
@@ -0,0 +1,80 @@
+---
+name: uss-bem-checker
+description: Reviews USS stylesheets and the C# strings that reference them against the Aspid.FastTools BEM grammar (class names) and the positional grammar (custom properties). Use after edits to any *.uss file or to any code holding USS class names / `--aspid-*` variables (Constants.cs, AspidStyles.cs, component .cs files).
+---
+
+You are a strict reviewer of UIToolkit USS conventions for the **Aspid.FastTools** Unity package. Both grammars below are mandatory and documented in the project root `CLAUDE.md`. Your only job is to verify that every USS class name and every custom property follows them, and to flag legacy forms.
+
+## Scope
+
+Files to review (only what was changed unless the user widens the scope):
+
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Unity/Editor/Resources/UI/**/*.uss`
+- C# files that emit class strings or read custom properties:
+ - `Unity/Editor/Scripts/Ids/Constants.cs` (`Constants.Drawer.*`, `Constants.Registry.*`, `Constants.Selector.*`)
+ - `Unity/Editor/Scripts/VisualElements/Internal/Styles/AspidStyles.cs`
+ - Component `.cs` under `Unity/Editor/Scripts/VisualElements/Internal/Components/**/`
+- Anywhere a literal `aspid-fasttools-...` or `--aspid-...` appears.
+
+## Grammar #1 — USS class names (BEM)
+
+Format: `aspid-fasttools-{block}[__{element}][--{modifier}]`
+
+Rules to enforce:
+
+1. The prefix `aspid-fasttools-` is mandatory and joined to the block by a single `-`.
+2. Block — kebab-case (`id-registry`, `enum-values`, `serializable-type`).
+3. Element — joined to block with `__` (double underscore): `aspid-fasttools-id-drawer__add-button`.
+4. Modifier — joined with `--` (double dash): `aspid-fasttools-id-registry__warning--visible`, `aspid-fasttools-status--error`.
+5. Inside any segment use kebab-case only — never `camelCase`, never single `_`.
+6. Utility/state classes (`status`, `theme`) are blocks of their own: `aspid-fasttools-status--error`, `aspid-fasttools-theme--dark`.
+
+**Legacy form to flag and propose migrating:** classes that use a single `-` between block and element instead of `__` (e.g. `aspid-fasttools-id-drawer-add-button`). The CLAUDE.md says: migrate when touching surrounding code; new classes must follow BEM from the start. So:
+- If the diff *adds* a non-BEM class → reject.
+- If the diff *modifies code around* a legacy class → suggest migrating it as part of the change, but don't block.
+
+## Grammar #2 — USS custom properties (positional)
+
+Format: `--{prefix}-{group}-{role}[-{state}][-{tone}]`
+
+Rules to enforce:
+
+| Slot | Allowed values | Required |
+|---|---|---|
+| `prefix` | `aspid` (palette shared between Aspid packages) or `aspid-fasttools` (product-specific) | yes |
+| `group` | `colors`, `icons`, `metrics`, `prop` | yes |
+| `role` | `bg`, `shade`, `text`, `border`, `icon`, `status`, `gradient`, `label_size`, `line_size`, `theme`, … | yes |
+| `state` | `success`, `warning`, `error`, `info`, `hover`, `pressed`, … | optional |
+| `tone` | `darkness`, `dark`, `light`, `lightness` | optional |
+
+Additional rules:
+
+1. Slot separator is `-`. Compound words **inside one slot** use `_` (e.g. `label_size`, `line_size`) — never two independent concepts in one slot.
+2. Order is **state → tone**: `--aspid-colors-status-success-darkness`, never `darkness-success`.
+3. Color roles:
+ - `bg` — surface palette.
+ - `shade` — generic content palette (text/border/icon-tint share the same shade swatch when not specialised).
+ - `text` / `border` / `icon` — specialised, component-local roles.
+ - `status` — `success` / `warning` / `error` / `info`.
+4. `prop` group is for inline component parameters (e.g. `--aspid-fasttools-prop-theme`), not palette tokens.
+5. Palette variables are declared on `:root`. Component-scoped variables on the component's selector.
+
+The reference implementation is `Aspid-FastTools-Default-Dark.uss` — palette tokens there are the source of truth.
+
+## How to review
+
+For each USS file or code string in scope:
+
+1. Extract every class name (`.aspid-fasttools-...`) and every custom property (`--aspid-...`).
+2. For each, validate against the matching grammar above.
+3. Categorise findings as:
+ - **Block** — adds a new non-conforming name. Must be fixed before merge.
+ - **Migrate** — touches surrounding code that already contains a legacy form. Suggest the rewrite, don't block.
+ - **OK** — conforming.
+4. Report concisely:
+ - File path + line.
+ - The offending name.
+ - Which rule it breaks.
+ - The corrected form.
+
+Do not propose stylistic changes (colors, spacing, ordering). Stay narrowly inside the two grammars and the legacy-migration rule. If the change does not touch USS classes or custom properties, return "No USS naming issues found." in one line.
diff --git a/.claude/hooks/rebuild-generators-on-change.sh b/.claude/hooks/rebuild-generators-on-change.sh
new file mode 100755
index 00000000..ae0c9e83
--- /dev/null
+++ b/.claude/hooks/rebuild-generators-on-change.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# PostToolUse hook: rebuild Roslyn source generators after edits inside the
+# main generator project, then redeploy the DLL into the Unity package.
+#
+# Path-scoped on purpose:
+# - Triggers ONLY for *.cs under Aspid.FastTools.Generators/Aspid.FastTools.Generators/
+# - Skips Unity-side edits (Aspid.FastTools/Assets/...), tests, and the Sample project.
+# - Skipping Unity edits matches the rule "do not run dotnet build for Unity-only edits".
+#
+# 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.Generators/Aspid.FastTools.Generators/*.cs) ;;
+ *) exit 0 ;;
+esac
+
+cd "$CLAUDE_PROJECT_DIR" || exit 0
+
+dotnet build \
+ Aspid.FastTools.Generators/Aspid.FastTools.Generators/Aspid.FastTools.Generators.csproj \
+ -c Release --nologo -v quiet 1>&2 || exit 2
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000..548c507c
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,26 @@
+{
+ "extraKnownMarketplaces": {
+ "unicli": {
+ "source": {
+ "source": "github",
+ "repo": "yucchiy/UniCli"
+ }
+ }
+ },
+ "enabledPlugins": {
+ "unicli@unicli": true
+ },
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": "Edit|Write",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rebuild-generators-on-change.sh\""
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.claude/skills/build-generator/SKILL.md b/.claude/skills/build-generator/SKILL.md
new file mode 100644
index 00000000..a2763a14
--- /dev/null
+++ b/.claude/skills/build-generator/SKILL.md
@@ -0,0 +1,13 @@
+---
+name: build-generator
+description: Build Roslyn source generators and deploy the resulting DLL into the Unity package
+user-invocable: true
+---
+
+Build the Aspid.FastTools source generators and deploy to Unity:
+
+1. Run `dotnet build Aspid.FastTools.Generators/Aspid.FastTools.Generators/Aspid.FastTools.Generators.csproj -c Release` from the repository root
+2. Copy `Aspid.FastTools.Generators/Aspid.FastTools.Generators/bin/Release/netstandard2.0/Aspid.FastTools.Generators.dll` to `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll`
+3. Report the result: build output, any errors, and confirm the DLL was copied successfully
+
+Arguments: $ARGUMENTS (optional: pass `Debug` to build in Debug configuration instead of Release)
diff --git a/.claude/skills/open-pr/SKILL.md b/.claude/skills/open-pr/SKILL.md
new file mode 100644
index 00000000..8e447325
--- /dev/null
+++ b/.claude/skills/open-pr/SKILL.md
@@ -0,0 +1,81 @@
+---
+name: open-pr
+description: Open a pull request following Aspid.FastTools project conventions — title format, PULL_REQUEST_TEMPLATE body, label policy, commit-message rules, and scope hygiene. Use this skill whenever the user asks to open / create / draft a PR, when running `@claude` triggers a PR-creation flow, or before invoking `gh pr create` directly.
+user-invocable: true
+---
+
+Use this skill any time a pull request is being opened in the Aspid.FastTools repository — manual, scripted, or via `@claude` automation.
+
+## Title
+
+- Short imperative sentence describing the change. Aim for under 70 characters.
+- No auto-generated branch-name strings. `Claude/add onenable idregistry i bk jl` is **not** acceptable; rewrite it.
+- Examples that pass: `Mark IdRegistry cache dirty on OnEnable`, `Fix this.Marker() generator for explicit interfaces, generics and field naming`, `Document PR conventions in CLAUDE.md`.
+
+## Body
+
+Fill out `.github/PULL_REQUEST_TEMPLATE.md` — three sections, all of which may be omitted only when truly empty.
+
+### `## Summary`
+
+- 1–3 bullets covering what changed and why.
+- Code snippets are welcome here when they make the change concrete (e.g. the new method body, a small before/after).
+- Do not paste a wall of release-notes prose; if the change really is that big, see the *Release-notes mega-PRs* carve-out below.
+
+### `## Notes for review`
+
+- Optional. Use it for trade-offs, risks, things you specifically want eyes on.
+- Also use it to flag unrelated noise in the diff (e.g. `.github/` files that already landed on `main` showing up because the branch sits ahead of `Develop`) so reviewers know it is a no-op on merge.
+- If there is nothing to say, delete the section — do not leave a placeholder.
+
+### `## Linked issues`
+
+- `Closes #N` for issues this PR fully resolves (auto-closes them on merge).
+- `Refs #N` for related issues that the PR does not close.
+- Delete the section when nothing applies.
+
+## Labels
+
+Pick from the existing label set; do **not** invent new labels in PRs. The label catalogue is fixed and visible via `gh label list --repo VPDPersonal/Aspid.FastTools`.
+
+| Group | Rule |
+|---|---|
+| `type: *` | Exactly **one**: `feature`, `fix`, `refactor`, `documentation`, `test`, `chore`, `ci`, `style`, `performance`. |
+| `area: *` | One or more for every part of the codebase the PR actually touches: `runtime`, `editor`, `generator`, `samples`. |
+| `status: *` | `needs-review` once the PR is ready, `work-in-progress` while still drafting. |
+| Special | `breaking-change`, `dependencies`, `needs-changelog` only when literally true. |
+
+## Commit messages
+
+- Short imperative sentences: `Add X`, `Fix Y`, `Mark Z`. Match the headline style already in `git log`.
+- **Never** append `Co-Authored-By: Claude …` or any other Claude/Anthropic attribution trailer. Commits are authored as the human user only. This overrides default templates from any skill (e.g. `commit-commands:commit`, `commit-commands:commit-push-pr`).
+
+## Scope
+
+- One logical change per PR.
+- If the diff drags in unrelated noise from a base-branch merge, do **not** delete it — just call it out under *Notes for review*.
+- Templates (`bug_report.yml`, `feature_request.yml`, `config.yml`, `PULL_REQUEST_TEMPLATE.md`) and CI workflows (`.github/workflows/*.yml`) live on `main`; targeting them at `Develop` directly is wrong unless the change is meant to ride a release merge.
+
+## Release-notes mega-PRs (carve-out)
+
+`Develop` → `main` release cuts (e.g. #8) are **exempt** from the three-section template. Use feature-scoped `###` subsections instead — one per major area (e.g. `### Source generators`, `### ID System`, `### UIToolkit`, `### Layout & rename`, `### Docs & tooling`). Mirror the structure of the most recent release PR.
+
+## Recipe
+
+When invoked, walk through this checklist before reporting "PR opened":
+
+1. **Branch** — confirm the head branch is named per existing patterns (`Feature/`, `chore/`, `claude/`). If the auto-name is ugly (e.g. `claude/add-onenable-idregistry-IBkJl`), live with it but compensate with a clean PR title.
+2. **Diff** — run `git diff ...HEAD --stat` and skim what's actually changing. Identify the *one* logical change vs. accidental noise.
+3. **Title** — draft per the rules above.
+4. **Body** — fill the three template sections. Use `gh pr create --body "$(cat <<'EOF' ... EOF)"` with a HEREDOC; never inline backticks through zsh, they get eaten as command substitution and silently drop content (this happened on issue-comment 4415890039 — see the `gh api -X PATCH` recovery).
+5. **Labels** — apply via `--add-label "type: X,area: Y,…"` either at create time or right after.
+6. **Closes / Refs** — if any issues are involved, write them in *Linked issues*. Verify with `gh issue list --state open` what is actually linkable.
+7. **Verify** — `gh pr view --json title,labels,body` to confirm everything took.
+
+## Common failure modes to avoid
+
+- Empty body. Fix immediately if you see `body=""` on a PR you opened.
+- Auto-generated title from branch name (`Claude/add-…`).
+- Two `type: *` labels at once (pick one).
+- Pasting unredacted Claude-attribution trailers in commit messages.
+- Backtick-inside-`gh-pr-comment` shell injection (always heredoc).
diff --git a/.claude/skills/sync-readmes/SKILL.md b/.claude/skills/sync-readmes/SKILL.md
new file mode 100644
index 00000000..15dd420a
--- /dev/null
+++ b/.claude/skills/sync-readmes/SKILL.md
@@ -0,0 +1,81 @@
+---
+name: sync-readmes
+description: Verify and update Aspid.FastTools README files against the actual codebase — namespaces, public API, CreateAssetMenu paths — keeping EN/RU and root/Documentation copies in sync
+user-invocable: true
+---
+
+The package ships **eight** README files that drift from the code easily. Use this skill whenever the user asks to "check / update / sync READMEs", or after any change that touches: namespaces of public types, public API surface, `[CreateAssetMenu]` paths, source generator output, or sample structure.
+
+## Files in scope
+
+**Main READMEs (mirror each other 1:1 except for image paths and one heading):**
+
+| Path | Locale | Image base path |
+|---|---|---|
+| `README.md` | EN | `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/` |
+| `README_RU.md` | RU | `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/` |
+| `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/README.md` | EN | `Images/` |
+| `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/README_RU.md` | RU | `Images/` |
+
+The Documentation copies have an extra `## Source Code` / `## Исходный код` block linking to the GitHub repo — the root copies don't. Otherwise the body is identical character-for-character.
+
+**Sample READMEs (one EN + one RU per sample):**
+
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples/Types/`
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples/Ids/`
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples/EnumValues/`
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples/ProfilerMarkers/`
+- `Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples/VisualElements/`
+
+## Workflow
+
+### 1. Verify against source before editing
+
+For every fact the README states, prove it from the code:
+
+| Claim | Verify with |
+|---|---|
+| Namespace of a public type | `grep -rn "namespace " --include="*.cs"` then locate the file declaring the type |
+| `[CreateAssetMenu]` menu path | `grep -rn "CreateAssetMenu\|menuName" --include="*.cs"` |
+| Public method signature / return value | Read the source file directly; do not infer from name |
+| Generated code shape (`IdStructGenerator`, `ProfilerMarkersGenerator`) | Read `Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/.../*Body.cs` |
+| Class actually exists | `find … -name ".cs"` — not all helper classes documented in old READMEs still exist (e.g. there is **no** `AspidEditorGUILayout`) |
+
+Common drift points discovered historically:
+
+- **Namespaces split per feature.** Public types live in `Aspid.FastTools` (root: `IId`, `UniqueIdAttribute`, `StringIdRegistry`), `Aspid.FastTools.Types`, `Aspid.FastTools.Enums`, `Aspid.FastTools.Ids`, `Aspid.FastTools.UIElements`. Editor helpers split similarly: `Aspid.FastTools.Editors` for `SerializedProperty` extensions / IMGUI scopes / `GetScriptName`, but per-feature editor code lives in `Aspid.FastTools.{Feature}.Editors`. A `using Aspid.FastTools;` line in a `SerializableType` example is wrong — it must be `using Aspid.FastTools.Types;`.
+- **Two ID registries.** `StringIdRegistry` (in `Aspid.FastTools`) keeps int↔string at runtime; `IdRegistry` (in `Aspid.FastTools.Ids`) is int-only at runtime with names stripped from player builds. Don't conflate them. Their menu paths differ: `Aspid/FastTools/String Id Registry` vs `Aspid/FastTools/Id Registry`. `StringIdRegistry.GetId` returns `-1` (not `0`) when not found; the lookup-by-id method is `GetNameId(int)`, not `GetName(int)`. Neither registry exposes public `Add`, `Remove`, or `Rename` — those live behind the registry inspector / `RegistryEditorCore`.
+- **Sample asset menu order.** Samples use `Aspid/Samples/FastTools/` (Samples first), not `Aspid/FastTools/Samples/`. Always re-grep `[CreateAssetMenu]` instead of trusting the existing README.
+
+### 2. Apply edits to all matching files
+
+Most edits hit all four main READMEs (EN root, EN Documentation, RU root, RU Documentation). Apply the same change to each — they must stay textually identical inside their respective body except for the image paths and the `## Source Code` heading.
+
+For RU edits, follow the existing RU translation conventions in the file: `Namespace` → `Пространство имён`, `Description` → `Описание`, code identifiers and English technical terms like `runtime`, `partial struct`, `Inspector` stay in English.
+
+When updating sample READMEs, edit both the EN and RU copy in the same sample folder.
+
+### 3. Sanity-check after editing
+
+Run these commands from the repo root and skim the output:
+
+```bash
+# All using statements in samples — these are ground truth for namespaces
+grep -rn "^using Aspid" Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples --include="*.cs" | sort -u
+
+# All CreateAssetMenu paths in the package
+grep -rn "menuName" Aspid.FastTools/Assets/Plugins/Aspid/FastTools --include="*.cs"
+
+# Confirm both READMEs of a pair stay aligned (count sections)
+grep -c "^## " README.md Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/README.md
+grep -c "^## " README_RU.md Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/README_RU.md
+```
+
+Then visually diff each pair of matched files (EN root vs EN Documentation; RU root vs RU Documentation) — only image paths and the `## Source Code` block should differ.
+
+## Arguments
+
+`$ARGUMENTS` (optional):
+- empty — full audit and update of all eight READMEs;
+- `--check` — audit only, report findings without editing;
+- a feature name (`ids`, `types`, `enums`, `visualelements`, `profilermarkers`, `imgui`, `serializedproperty`) — narrow the audit/update to that section.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..63e8cd43
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,41 @@
+name: Bug report
+description: Report a bug or unexpected behavior in Aspid.FastTools.
+labels: ["type: fix"]
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: What went wrong? What did you expect instead?
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce
+ description: Minimal steps so the bug can be reproduced locally.
+ placeholder: |
+ 1. ...
+ 2. ...
+ 3. ...
+ validations:
+ required: true
+ - type: input
+ id: unity-version
+ attributes:
+ label: Unity version
+ placeholder: "2022.3.x"
+ validations:
+ required: true
+ - type: input
+ id: package-version
+ attributes:
+ label: Aspid.FastTools version
+ placeholder: "1.0.0"
+ validations:
+ required: true
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional context
+ description: Logs, screenshots, related issues — anything that helps.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..f0c07480
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Questions and ideas
+ url: https://github.com/VPDPersonal/Aspid.FastTools/discussions
+ about: Open-ended questions, design ideas and Q&A go in Discussions, not Issues.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..451683f5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,27 @@
+name: Feature request
+description: Suggest a new capability or improvement for Aspid.FastTools.
+labels: ["type: feature"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem
+ description: What problem does this solve? Who is it for?
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposal
+ description: Sketch of the API or behavior you'd like — code examples are welcome.
+ validations:
+ required: true
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives
+ description: Other approaches you considered, and why this one is preferred.
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional context
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..456d16e4
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,11 @@
+## Summary
+
+
+
+## Notes for review
+
+
+
+## Linked issues
+
+
diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
new file mode 100644
index 00000000..b5e8cfd4
--- /dev/null
+++ b/.github/workflows/claude-code-review.yml
@@ -0,0 +1,44 @@
+name: Claude Code Review
+
+on:
+ pull_request:
+ types: [opened, synchronize, ready_for_review, reopened]
+ # Optional: Only run on specific file changes
+ # paths:
+ # - "src/**/*.ts"
+ # - "src/**/*.tsx"
+ # - "src/**/*.js"
+ # - "src/**/*.jsx"
+
+jobs:
+ claude-review:
+ # Optional: Filter by PR author
+ # if: |
+ # github.event.pull_request.user.login == 'external-contributor' ||
+ # github.event.pull_request.user.login == 'new-developer' ||
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code Review
+ id: claude-review
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
+ plugins: 'code-review@claude-code-plugins'
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://code.claude.com/docs/en/cli-reference for available options
+
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 00000000..6b15fac7
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,50 @@
+name: Claude Code
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_review_comment:
+ types: [created]
+ issues:
+ types: [opened, assigned]
+ pull_request_review:
+ types: [submitted]
+
+jobs:
+ claude:
+ if: |
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+ actions: read # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code
+ id: claude
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ # This is an optional setting that allows Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
+
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
+ # prompt: 'Update the pull request description to include a summary of changes.'
+
+ # Optional: Add claude_args to customize behavior and configuration
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://code.claude.com/docs/en/cli-reference for available options
+ # claude_args: '--allowed-tools Bash(gh pr *)'
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..ca6f4bb0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.claude/settings.local.json
+.worktrees/
+
+# UPM convention: `Samples~` is hidden from Unity importer but must be tracked.
+# Override global `*~` ignore.
+!Samples~/
+!Samples~/**
diff --git a/.gitmodules b/.gitmodules
index e69de29b..a40945e0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "Aspid.FastTools.Claude"]
+ path = Aspid.FastTools.Claude
+ url = git@github.com:VPDPersonal/Aspid.FastTools.Claude.git
diff --git a/Aspid.FastTools.Claude b/Aspid.FastTools.Claude
new file mode 160000
index 00000000..79ba24c2
--- /dev/null
+++ b/Aspid.FastTools.Claude
@@ -0,0 +1 @@
+Subproject commit 79ba24c2d529b3f5ea0c4774e9d1b22870073c7b
diff --git a/Aspid.UnityFastTools.Generators/.gitignore b/Aspid.FastTools.Generators/.gitignore
similarity index 100%
rename from Aspid.UnityFastTools.Generators/.gitignore
rename to Aspid.FastTools.Generators/.gitignore
diff --git a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Sample/Aspid.UnityFastTools.Generators.Sample.csproj b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Sample/Aspid.FastTools.Generators.Sample.csproj
similarity index 61%
rename from Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Sample/Aspid.UnityFastTools.Generators.Sample.csproj
rename to Aspid.FastTools.Generators/Aspid.FastTools.Generators.Sample/Aspid.FastTools.Generators.Sample.csproj
index c7d1c6e2..c971c512 100644
--- a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Sample/Aspid.UnityFastTools.Generators.Sample.csproj
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Sample/Aspid.FastTools.Generators.Sample.csproj
@@ -3,13 +3,13 @@
net6.0
enable
- UnityFastToolsGenerators.Sample
+ Aspid.FastTools.Sample
6000.2.7f2
9
-
+
diff --git a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Tests/Aspid.UnityFastTools.Generators.Tests.csproj b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Aspid.FastTools.Generators.Tests.csproj
similarity index 79%
rename from Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Tests/Aspid.UnityFastTools.Generators.Tests.csproj
rename to Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Aspid.FastTools.Generators.Tests.csproj
index 963f0781..a09224de 100644
--- a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators.Tests/Aspid.UnityFastTools.Generators.Tests.csproj
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Aspid.FastTools.Generators.Tests.csproj
@@ -3,10 +3,11 @@
net6.0
enable
+ latest
false
- UnityFastToolsGenerators.Tests
+ Aspid.FastTools.Tests
@@ -20,7 +21,7 @@
-
+
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Helpers/GeneratorTestHost.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Helpers/GeneratorTestHost.cs
new file mode 100644
index 00000000..d75a5060
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/Helpers/GeneratorTestHost.cs
@@ -0,0 +1,113 @@
+using Xunit;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis.CSharp;
+using Aspid.FastTools.Generators.IdStruct;
+using Aspid.FastTools.Generators.ProfilerMarkers;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Generators.Tests.Helpers;
+
+internal static class GeneratorTestHost
+{
+ public const string IIdDefinition = """
+ namespace Aspid.FastTools.Ids
+ {
+ public interface IId { int Id { get; } }
+ }
+ """;
+
+ public const string UnityEngineStubs = """
+ namespace UnityEngine
+ {
+ [System.AttributeUsage(System.AttributeTargets.Field)]
+ public sealed class SerializeField : System.Attribute { }
+ }
+ """;
+
+ public const string ProfilerMarkerStubs = """
+ namespace Unity.Profiling
+ {
+ public struct ProfilerMarker
+ {
+ public ProfilerMarker(string _) { }
+ public AutoScope Auto() => default;
+ public struct AutoScope : System.IDisposable { public void Dispose() { } }
+ }
+ }
+
+ public static class ProfilerMarkerExtensionsForGenerator
+ {
+ public static Unity.Profiling.ProfilerMarker.AutoScope Marker(this object _) => default;
+ public static Unity.Profiling.ProfilerMarker.AutoScope WithName(this in Unity.Profiling.ProfilerMarker.AutoScope marker, string _) => marker;
+ }
+ """;
+
+ public static GeneratorRun RunIdStruct(string userSource)
+ {
+ var compilation = BuildCompilation(new[] { userSource, IIdDefinition, UnityEngineStubs });
+ return Run(compilation, new IdStructGenerator());
+ }
+
+ public static GeneratorRun RunProfilerMarkers(string userSource)
+ {
+ var compilation = BuildCompilation(new[] { userSource, ProfilerMarkerStubs });
+ return Run(compilation, new ProfilerMarkersGenerator());
+ }
+
+ public static void AssertNoErrors(GeneratorRun run)
+ {
+ var generatorErrors = run.RunResult.Diagnostics
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToArray();
+ Assert.True(
+ generatorErrors.Length == 0,
+ "Generator emitted errors: " + string.Join("; ", generatorErrors.Select(d => d.ToString())));
+
+ var compileErrors = run.OutputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToArray();
+ Assert.True(
+ compileErrors.Length == 0,
+ "Generated source has compile errors: " + string.Join("; ", compileErrors.Select(d => d.ToString())));
+ }
+
+ private static GeneratorRun Run(CSharpCompilation compilation, IIncrementalGenerator generator)
+ {
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out _);
+ return new GeneratorRun(driver, driver.GetRunResult(), output);
+ }
+
+ private static CSharpCompilation BuildCompilation(IEnumerable sources)
+ {
+ var trees = sources.Select(s => CSharpSyntaxTree.ParseText(s));
+ var references = new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.CallerLineNumberAttribute).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.ComponentModel.EditorBrowsableAttribute).Assembly.Location),
+ };
+
+ return CSharpCompilation.Create(
+ assemblyName: "TestCompilation",
+ syntaxTrees: trees,
+ references: references,
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
+
+internal readonly struct GeneratorRun
+{
+ public readonly GeneratorDriver Driver;
+ public readonly GeneratorDriverRunResult RunResult;
+ public readonly Compilation OutputCompilation;
+
+ public GeneratorRun(GeneratorDriver driver, GeneratorDriverRunResult runResult, Compilation outputCompilation)
+ {
+ Driver = driver;
+ RunResult = runResult;
+ OutputCompilation = outputCompilation;
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IdStructGeneratorTests.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IdStructGeneratorTests.cs
new file mode 100644
index 00000000..3fc44618
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IdStructGeneratorTests.cs
@@ -0,0 +1,350 @@
+using Xunit;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Aspid.FastTools.Generators.Tests.Helpers;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Generators.Tests;
+
+public class IdStructGeneratorTests
+{
+ [Fact]
+ public void Generator_DoesNotCrash_OnEmptySource()
+ {
+ var run = GeneratorTestHost.RunIdStruct("namespace Test { }");
+
+ Assert.Empty(run.RunResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+ }
+
+ [Fact]
+ public void Struct_WithIId_InNamespace_GeneratesIdFieldAndProperty()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId { }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ Assert.EndsWith(".IId.g.cs", generated[0].HintName);
+
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial struct Foo", text);
+ Assert.Contains("private int _id;", text);
+ Assert.Contains("public int Id =>", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_WithoutPartial_EmitsAFID001AndDoesNotGenerate()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public struct Foo : global::Aspid.FastTools.Ids.IId
+ {
+ public int Id => 0;
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+
+ var diags = run.RunResult.Diagnostics;
+ Assert.Contains(diags, d => d.Id == "AFID001" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void Struct_WithExistingId_EmitsAFID002AndDoesNotGenerate()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId
+ {
+ private int _id;
+ public int Id => _id;
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+
+ var afid002 = run.RunResult.Diagnostics.FirstOrDefault(d => d.Id == "AFID002");
+ Assert.NotNull(afid002);
+ Assert.Equal(DiagnosticSeverity.Error, afid002!.Severity);
+ var msg = afid002.GetMessage();
+ Assert.Contains("_id", msg);
+ Assert.Contains("Id", msg);
+ }
+
+ [Fact]
+ public void Struct_WithoutIId_NotGenerated()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public interface IMarker { }
+ public partial struct Foo : IMarker { }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+ }
+
+ [Fact]
+ public void TwoStructsSameName_DifferentNamespaces_NoHintNameCollision()
+ {
+ const string source = """
+ namespace SampleA
+ {
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId { }
+ }
+
+ namespace SampleB
+ {
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId { }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Equal(2, generated.Length);
+
+ var hintNames = generated.Select(s => s.HintName).ToArray();
+ Assert.NotEqual(hintNames[0], hintNames[1]);
+ Assert.Contains(hintNames, h => h.Contains("SampleA"));
+ Assert.Contains(hintNames, h => h.Contains("SampleB"));
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void NestedStruct_WrappedInPartialOuterClass()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial class Outer
+ {
+ public partial struct Inner : global::Aspid.FastTools.Ids.IId { }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial class Outer", text);
+ Assert.Contains("partial struct Inner", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void NestedStruct_InGenericOuter_EmitsTypeParameters()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial class Outer
+ {
+ public partial struct Inner : global::Aspid.FastTools.Ids.IId { }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial class Outer", text);
+ Assert.Contains("partial struct Inner", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_InGlobalNamespace_GeneratesWithoutNamespaceBlock()
+ {
+ const string source = "public partial struct GlobalFoo : global::Aspid.FastTools.Ids.IId { }";
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial struct GlobalFoo", text);
+ Assert.DoesNotContain("namespace ", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_FileScopedNamespace_Generates()
+ {
+ const string source = """
+ namespace Sample;
+
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId { }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("namespace Sample", text);
+ Assert.Contains("partial struct Foo", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_ThreeLevelNesting_WrappedInCorrectOrder()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial class Outer
+ {
+ public partial class Middle
+ {
+ public partial struct Inner : global::Aspid.FastTools.Ids.IId { }
+ }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial class Outer", text);
+ Assert.Contains("partial class Middle", text);
+ Assert.Contains("partial struct Inner", text);
+
+ // Verify Outer wraps Middle wraps Inner.
+ var outerIdx = text.IndexOf("partial class Outer", System.StringComparison.Ordinal);
+ var middleIdx = text.IndexOf("partial class Middle", System.StringComparison.Ordinal);
+ var innerIdx = text.IndexOf("partial struct Inner", System.StringComparison.Ordinal);
+ Assert.True(outerIdx < middleIdx && middleIdx < innerIdx, "Wrappers must be ordered Outer → Middle → Inner");
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_TransitiveIId_Generates()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public interface IMyId : global::Aspid.FastTools.Ids.IId { }
+ public partial struct Foo : IMyId { }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial struct Foo", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_GenericTarget_EmitsTypeParameters()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial struct MyId : global::Aspid.FastTools.Ids.IId { }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial struct MyId", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Struct_RecordStructContaining_GeneratesRecordWrapper()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial record struct Outer
+ {
+ public partial struct Inner : global::Aspid.FastTools.Ids.IId { }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("partial record struct Outer", text);
+ Assert.Contains("partial struct Inner", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void TwoStructs_InGenericOuters_DifferentArity_NoHintCollision()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public partial class Outer
+ {
+ public partial struct MyId : global::Aspid.FastTools.Ids.IId { }
+ }
+
+ public partial class Outer
+ {
+ public partial struct MyId : global::Aspid.FastTools.Ids.IId { }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunIdStruct(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Equal(2, generated.Length);
+
+ var hintNames = generated.Select(s => s.HintName).ToArray();
+ Assert.NotEqual(hintNames[0], hintNames[1]);
+
+ var combined = string.Concat(generated.Select(s => s.SourceText.ToString()));
+ Assert.Contains("partial class Outer", combined);
+ Assert.Contains("partial class Outer", combined);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IncrementalCacheTests.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IncrementalCacheTests.cs
new file mode 100644
index 00000000..65d81242
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/IncrementalCacheTests.cs
@@ -0,0 +1,116 @@
+using Xunit;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Aspid.FastTools.Generators.IdStruct;
+using Aspid.FastTools.Generators.ProfilerMarkers;
+using Aspid.FastTools.Generators.Tests.Helpers;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Generators.Tests;
+
+// Verifies that running the same generator twice over compilations that differ only
+// in an *unrelated* file produces cached pipeline steps. Regression guard for the
+// "ISymbol stored in data structures" anti-pattern that defeats the cache.
+public class IncrementalCacheTests
+{
+ [Fact]
+ public void IdStruct_UnrelatedSourceChange_PreservesCache()
+ {
+ const string targetSource = """
+ namespace Sample
+ {
+ public partial struct Foo : global::Aspid.FastTools.Ids.IId { }
+ }
+ """;
+
+ AssertCachedAfterUnrelatedEdit(
+ targetSource,
+ new IdStructGenerator(),
+ useUnityStubs: true);
+ }
+
+ [Fact]
+ public void ProfilerMarkers_UnrelatedSourceChange_PreservesCache()
+ {
+ const string targetSource = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker(); }
+ }
+ }
+ """;
+
+ AssertCachedAfterUnrelatedEdit(
+ targetSource,
+ new ProfilerMarkersGenerator(),
+ useUnityStubs: false);
+ }
+
+ private static void AssertCachedAfterUnrelatedEdit(string targetSource, IIncrementalGenerator generator, bool useUnityStubs)
+ {
+ var stubs = useUnityStubs
+ ? new[] { GeneratorTestHost.IIdDefinition, GeneratorTestHost.UnityEngineStubs }
+ : new[] { GeneratorTestHost.ProfilerMarkerStubs };
+
+ var driverOptions = new GeneratorDriverOptions(
+ disabledOutputs: IncrementalGeneratorOutputKind.None,
+ trackIncrementalGeneratorSteps: true);
+
+ var compilation1 = MakeCompilation(targetSource, "// version 1", stubs);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(
+ generators: new[] { generator.AsSourceGenerator() },
+ additionalTexts: null,
+ parseOptions: null,
+ optionsProvider: null,
+ driverOptions: driverOptions);
+
+ driver = driver.RunGenerators(compilation1);
+
+ // Second run with the unrelated tree edited; the target tree is unchanged.
+ var compilation2 = MakeCompilation(targetSource, "// version 2", stubs);
+ driver = driver.RunGenerators(compilation2);
+
+ var result = driver.GetRunResult().Results.Single();
+
+ // Inspect the steps from the *second* run. Steps tied to the target source
+ // must show all-Cached / Unchanged outputs (the user code didn't change).
+ var trackedSteps = result.TrackedOutputSteps.SelectMany(kvp => kvp.Value).ToArray();
+ Assert.NotEmpty(trackedSteps);
+
+ foreach (var step in trackedSteps)
+ {
+ foreach (var (_, reason) in step.Outputs)
+ {
+ Assert.True(
+ reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged,
+ $"Output step '{step.Name}' was '{reason}', expected Cached/Unchanged. " +
+ "This indicates a non-equatable value somewhere in the pipeline.");
+ }
+ }
+ }
+
+ private static CSharpCompilation MakeCompilation(string targetSource, string unrelatedSource, string[] stubs)
+ {
+ var trees = new[]
+ {
+ CSharpSyntaxTree.ParseText(targetSource, path: "Target.cs"),
+ CSharpSyntaxTree.ParseText(unrelatedSource, path: "Unrelated.cs"),
+ }.Concat(stubs.Select(s => CSharpSyntaxTree.ParseText(s)));
+
+ var references = new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.CallerLineNumberAttribute).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.ComponentModel.EditorBrowsableAttribute).Assembly.Location),
+ };
+
+ return CSharpCompilation.Create(
+ assemblyName: "TestCompilation",
+ syntaxTrees: trees,
+ references: references,
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/ProfilerMarkersGeneratorTests.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/ProfilerMarkersGeneratorTests.cs
new file mode 100644
index 00000000..2d535d0c
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.Tests/ProfilerMarkersGeneratorTests.cs
@@ -0,0 +1,445 @@
+using Xunit;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Aspid.FastTools.Generators.Tests.Helpers;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Generators.Tests;
+
+public class ProfilerMarkersGeneratorTests
+{
+ [Fact]
+ public void Generator_DoesNotCrash_OnEmptySource()
+ {
+ var run = GeneratorTestHost.RunProfilerMarkers("namespace Test { }");
+
+ Assert.Empty(run.RunResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+ }
+
+ [Fact]
+ public void SingleMarkerCall_GeneratesExtensionClass()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("internal static class __FooProfilerMarkerExtensions", text);
+ Assert.Contains("Run_Marker_Line_", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void TwoCallsOnDifferentLines_GenerateTwoFields()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run()
+ {
+ this.Marker();
+ this.Marker();
+ }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ // Two distinct fields, named after method + their line numbers.
+ var fieldDeclarations = System.Text.RegularExpressions.Regex.Matches(
+ text, @"static\s+readonly\s+global::Unity\.Profiling\.ProfilerMarker\s+(\w+)\s*=");
+ Assert.Equal(2, fieldDeclarations.Count);
+
+ var name1 = fieldDeclarations[0].Groups[1].Value;
+ var name2 = fieldDeclarations[1].Groups[1].Value;
+ Assert.NotEqual(name1, name2);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void TwoCallsOnSameLine_DedupesFieldNames()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker(); this.Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ // Two distinct fields even though they're on the same source line.
+ var fieldDeclarations = System.Text.RegularExpressions.Regex.Matches(
+ text, @"static\s+readonly\s+global::Unity\.Profiling\.ProfilerMarker\s+(\w+)\s*=");
+ Assert.Equal(2, fieldDeclarations.Count);
+
+ var name1 = fieldDeclarations[0].Groups[1].Value;
+ var name2 = fieldDeclarations[1].Groups[1].Value;
+ Assert.NotEqual(name1, name2);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void WithNameLiteral_OverridesMarkerLabel()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker().WithName("CustomLabel"); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("CustomLabel", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void WithNameInterpolated_PlainText_OverridesMarkerLabel()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker().WithName($"MyMarker"); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("MyMarker", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void WithNameAfterParenthesis_OverridesMarkerLabel()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { (this.Marker()).WithName("ParenLabel"); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("ParenLabel", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void WithNameInterpolatedWithVariable_FallsBackToMethodName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { var x = 42; this.Marker().WithName($"X{x}"); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ // Variable interpolation can't be evaluated at compile time → label is the method name.
+ Assert.Contains("Run_Marker_Line_", text);
+ Assert.DoesNotContain("\"X{x}\"", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Constructor_UsesCtorAsMarkerName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public Foo() { this.Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("Ctor_Marker_Line_", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void PropertyGetter_UsesPropertyNameAsMarkerName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public int Value
+ {
+ get { this.Marker(); return 0; }
+ }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("Value_Marker_Line_", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void GenericClass_EmitsTypeParameters()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("__Foo_1ProfilerMarkerExtensions", text);
+ Assert.Contains("Markers", text);
+ Assert.Contains("Marker(this global::Sample.Foo", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void GlobalNamespace_GeneratesWithoutNamespaceBlock()
+ {
+ const string source = """
+ public class Foo
+ {
+ public void Run() { this.Marker(); }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.DoesNotContain("namespace ", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void TwoTypesSameName_DifferentNamespaces_NoHintCollision()
+ {
+ const string source = """
+ namespace SampleA
+ {
+ public class Foo { public void Run() { this.Marker(); } }
+ }
+ namespace SampleB
+ {
+ public class Foo { public void Run() { this.Marker(); } }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Equal(2, generated.Length);
+ var hintNames = generated.Select(s => s.HintName).ToArray();
+ Assert.NotEqual(hintNames[0], hintNames[1]);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void NestedTypesSameName_DifferentOuters_NoHintCollision()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class OuterA { public class Inner { public void Run() { this.Marker(); } } }
+ public class OuterB { public class Inner { public void Run() { this.Marker(); } } }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Equal(2, generated.Length);
+ var hintNames = generated.Select(s => s.HintName).ToArray();
+ Assert.NotEqual(hintNames[0], hintNames[1]);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Lambda_UsesContainingMethodAsMarkerName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run()
+ {
+ System.Action a = () => this.Marker();
+ a();
+ }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ // Marker name must be the enclosing real method, not the synthesized lambda symbol.
+ Assert.Contains("Run_Marker_Line_", text);
+ Assert.DoesNotContain("<>", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void LocalFunction_UsesContainingMethodAsMarkerName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run()
+ {
+ Local();
+ void Local() { this.Marker(); }
+ }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("Run_Marker_Line_", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void FieldInitializer_UsesFieldNameAsMarkerName()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ private static readonly int _count = ((object)null).Marker() is var _ ? 1 : 0;
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var generated = run.RunResult.Results[0].GeneratedSources;
+
+ Assert.Single(generated);
+ var text = generated[0].SourceText.ToString();
+ Assert.Contains("_count_Marker_Line_", text);
+ }
+
+ [Fact]
+ public void UnrelatedMarkerExtension_IsNotProcessed()
+ {
+ // User defines their own Marker() extension on a custom type — the generator
+ // must NOT mistake it for ProfilerMarkerExtensionsForGenerator.Marker.
+ const string source = """
+ namespace Sample
+ {
+ public class Foo { }
+ public static class FooExtensions
+ {
+ public static int Marker(this Foo _) => 0;
+ }
+ public class Caller
+ {
+ public void Run() { var x = new Foo().Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+
+ Assert.Empty(run.RunResult.Results[0].GeneratedSources);
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+
+ [Fact]
+ public void Dispatcher_IsGatedByEnableProfiler_AndFallsBackToDefault()
+ {
+ const string source = """
+ namespace Sample
+ {
+ public class Foo
+ {
+ public void Run() { this.Marker(); }
+ }
+ }
+ """;
+
+ var run = GeneratorTestHost.RunProfilerMarkers(source);
+ var text = run.RunResult.Results[0].GeneratedSources[0].SourceText.ToString();
+
+ Assert.Contains("#if ENABLE_PROFILER", text);
+ Assert.Contains("#endif", text);
+ Assert.Contains("return default;", text);
+ Assert.DoesNotContain("throw new", text);
+
+ GeneratorTestHost.AssertNoErrors(run);
+ }
+}
diff --git a/Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.sln b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.sln
similarity index 64%
rename from Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.sln
rename to Aspid.FastTools.Generators/Aspid.FastTools.Generators.sln
index aa25b9e4..29d5927d 100644
--- a/Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.sln
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators.sln
@@ -1,10 +1,10 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.UnityFastTools.Generators", "UnityFastToolsGenerators\Aspid.UnityFastTools.Generators\Aspid.UnityFastTools.Generators.csproj", "{CB9D8D51-7D86-4B84-A0DB-73E418962DA7}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.FastTools.Generators", "Aspid.FastTools.Generators\Aspid.FastTools.Generators.csproj", "{CB9D8D51-7D86-4B84-A0DB-73E418962DA7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.UnityFastTools.Generators.Sample", "UnityFastToolsGenerators\Aspid.UnityFastTools.Generators.Sample\Aspid.UnityFastTools.Generators.Sample.csproj", "{2835DD81-D105-4C2E-AE03-BC7D064C29D1}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.FastTools.Generators.Sample", "Aspid.FastTools.Generators.Sample\Aspid.FastTools.Generators.Sample.csproj", "{2835DD81-D105-4C2E-AE03-BC7D064C29D1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.UnityFastTools.Generators.Tests", "UnityFastToolsGenerators\Aspid.UnityFastTools.Generators.Tests\Aspid.UnityFastTools.Generators.Tests.csproj", "{F4953608-2F14-4B2E-B91C-B3FDFC81B180}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspid.FastTools.Generators.Tests", "Aspid.FastTools.Generators.Tests\Aspid.FastTools.Generators.Tests.csproj", "{F4953608-2F14-4B2E-B91C-B3FDFC81B180}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.csproj b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Aspid.FastTools.Generators.csproj
similarity index 73%
rename from Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.csproj
rename to Aspid.FastTools.Generators/Aspid.FastTools.Generators/Aspid.FastTools.Generators.csproj
index 74888a77..b3d7d6c4 100644
--- a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators/Aspid.UnityFastTools.Generators.csproj
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Aspid.FastTools.Generators.csproj
@@ -3,11 +3,17 @@
netstandard2.0
enable
- 13
+ latest
true
- UnityFastToolsGenerators
+ Aspid.FastTools
+
+ $(NoWarn);RS2008
+
+
+
+
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Descriptions/General.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Descriptions/General.cs
new file mode 100644
index 00000000..51a88c84
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Descriptions/General.cs
@@ -0,0 +1,9 @@
+using Aspid.Generators.Helper;
+
+namespace Aspid.FastTools.Descriptions;
+
+internal static class General
+{
+ public static readonly string ProfilerMarkerGeneratedCode = $"""{Classes.GeneratedCodeAttribute}("Aspid.FastTools.Generators.ProfilerMarkersGenerator", "1.0.0")""";
+ public static readonly string IdStructGeneratedCode = $"""{Classes.GeneratedCodeAttribute}("Aspid.FastTools.Generators.IdStructGenerator", "1.0.0")""";
+}
\ No newline at end of file
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Bodies/IdStructBody.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Bodies/IdStructBody.cs
new file mode 100644
index 00000000..d1d16d04
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Bodies/IdStructBody.cs
@@ -0,0 +1,58 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Aspid.Generators.Helper;
+using Aspid.FastTools.Generators.IdStruct.Data;
+using static Aspid.FastTools.Descriptions.General;
+
+namespace Aspid.FastTools.Generators.IdStruct.Bodies;
+
+internal static class IdStructBody
+{
+ public static void GenerateCode(in SourceProductionContext context, in IdStructData data)
+ {
+ var hasNamespace = data.Namespace != null;
+ var nestedDepth = data.ContainingTypes.Length;
+
+ var code = new CodeWriter()
+ .AppendLine("// ")
+ .AppendLine()
+ .AppendLineIf(hasNamespace, $"namespace {data.Namespace}")
+ .BeginBlockIf(hasNamespace);
+
+ var nestedNames = new StringBuilder();
+ foreach (var containing in data.ContainingTypes)
+ {
+ code.AppendLine($"partial {containing.Keyword} {containing.Name}{containing.TypeParameters}")
+ .BeginBlock();
+
+ nestedNames.Append(containing.Name);
+ if (containing.Arity > 0) nestedNames.Append('_').Append(containing.Arity);
+ nestedNames.Append('.');
+ }
+
+ code.AppendLine($"partial struct {data.StructName}{data.TypeParameters}")
+ .BeginBlock()
+ .AppendLine("#if UNITY_EDITOR")
+ .AppendLine($"[{IdStructGeneratedCode}]")
+ .AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]")
+ .AppendLine("[global::UnityEngine.SerializeField] private string __stringId;")
+ .AppendLine("#endif")
+ .AppendLine($"[{IdStructGeneratedCode}]")
+ .AppendLine("[global::UnityEngine.SerializeField] private int _id;")
+ .AppendLine($"[{IdStructGeneratedCode}]")
+ .AppendLine("public int Id => _id;")
+ .EndBlock();
+
+ for (var i = 0; i < nestedDepth; i++)
+ code.EndBlock();
+
+ code.EndBlockIf(hasNamespace);
+
+ var aritySuffix = data.Arity > 0 ? "_" + data.Arity : string.Empty;
+ var hintName = hasNamespace
+ ? $"{data.Namespace}.{nestedNames}{data.StructName}{aritySuffix}.IId.g.cs"
+ : $"{nestedNames}{data.StructName}{aritySuffix}.IId.g.cs";
+
+ context.AddSource(hintName, code.GetSourceText());
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/ContainingTypeInfo.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/ContainingTypeInfo.cs
new file mode 100644
index 00000000..a6735661
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/ContainingTypeInfo.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace Aspid.FastTools.Generators.IdStruct.Data;
+
+internal readonly struct ContainingTypeInfo : IEquatable
+{
+ public readonly string Name;
+ public readonly string Keyword;
+ public readonly string TypeParameters;
+ public readonly int Arity;
+
+ public ContainingTypeInfo(string name, string keyword, string typeParameters, int arity)
+ {
+ Name = name;
+ Keyword = keyword;
+ TypeParameters = typeParameters;
+ Arity = arity;
+ }
+
+ public bool Equals(ContainingTypeInfo other) =>
+ Name == other.Name
+ && Keyword == other.Keyword
+ && TypeParameters == other.TypeParameters
+ && Arity == other.Arity;
+
+ public override bool Equals(object? obj) =>
+ obj is ContainingTypeInfo other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = Name.GetHashCode();
+ hash = (hash * 397) ^ Keyword.GetHashCode();
+ hash = (hash * 397) ^ TypeParameters.GetHashCode();
+ hash = (hash * 397) ^ Arity;
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/DiagnosticInfo.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/DiagnosticInfo.cs
new file mode 100644
index 00000000..3d985cca
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/DiagnosticInfo.cs
@@ -0,0 +1,73 @@
+using System;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Aspid.FastTools.Generators.IdStruct.Data;
+
+internal readonly struct DiagnosticInfo : IEquatable
+{
+ public readonly string DescriptorId;
+ public readonly string MessageArg0;
+ public readonly string? MessageArg1;
+ public readonly string? FilePath;
+ public readonly TextSpan TextSpan;
+ public readonly LinePositionSpan LineSpan;
+
+ public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, string messageArg0, string? messageArg1 = null)
+ {
+ DescriptorId = descriptor.Id;
+ MessageArg0 = messageArg0;
+ MessageArg1 = messageArg1;
+
+ if (location is { SourceTree: not null })
+ {
+ FilePath = location.SourceTree.FilePath;
+ TextSpan = location.SourceSpan;
+ LineSpan = location.GetLineSpan().Span;
+ }
+ else
+ {
+ FilePath = null;
+ TextSpan = default;
+ LineSpan = default;
+ }
+ }
+
+ public Diagnostic ToDiagnostic()
+ {
+ var descriptor = IdStructDiagnostics.GetDescriptor(DescriptorId);
+ if (descriptor is null) return Diagnostic.Create("UNKNOWN", "Generator", "Unknown descriptor", DiagnosticSeverity.Hidden, DiagnosticSeverity.Hidden, false, 4);
+
+ var location = FilePath is null
+ ? Location.None
+ : Location.Create(FilePath, TextSpan, LineSpan);
+
+ return MessageArg1 is null
+ ? Diagnostic.Create(descriptor, location, MessageArg0)
+ : Diagnostic.Create(descriptor, location, MessageArg0, MessageArg1);
+ }
+
+ public bool Equals(DiagnosticInfo other) =>
+ DescriptorId == other.DescriptorId
+ && MessageArg0 == other.MessageArg0
+ && MessageArg1 == other.MessageArg1
+ && FilePath == other.FilePath
+ && TextSpan.Equals(other.TextSpan)
+ && LineSpan.Equals(other.LineSpan);
+
+ public override bool Equals(object? obj) => obj is DiagnosticInfo other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = DescriptorId.GetHashCode();
+ hash = (hash * 397) ^ MessageArg0.GetHashCode();
+ hash = (hash * 397) ^ (MessageArg1?.GetHashCode() ?? 0);
+ hash = (hash * 397) ^ (FilePath?.GetHashCode() ?? 0);
+ hash = (hash * 397) ^ TextSpan.GetHashCode();
+ hash = (hash * 397) ^ LineSpan.GetHashCode();
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructData.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructData.cs
new file mode 100644
index 00000000..74c4c0ff
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructData.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+
+namespace Aspid.FastTools.Generators.IdStruct.Data;
+
+internal readonly struct IdStructData : IEquatable
+{
+ public readonly string StructName;
+ public readonly string TypeParameters;
+ public readonly int Arity;
+ public readonly string? Namespace;
+ public readonly ImmutableArray ContainingTypes;
+
+ public IdStructData(INamedTypeSymbol symbol)
+ {
+ StructName = symbol.Name;
+
+ var typeParams = symbol.TypeParameters;
+ Arity = typeParams.Length;
+ TypeParameters = Arity > 0
+ ? "<" + string.Join(", ", typeParams.Select(p => p.Name)) + ">"
+ : string.Empty;
+
+ Namespace = symbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : symbol.ContainingNamespace.ToDisplayString();
+
+ if (symbol.ContainingType is null)
+ {
+ ContainingTypes = ImmutableArray.Empty;
+ return;
+ }
+
+ var builder = ImmutableArray.CreateBuilder();
+ var current = symbol.ContainingType;
+ while (current is not null)
+ {
+ builder.Add(MakeContainingInfo(current));
+ current = current.ContainingType;
+ }
+
+ builder.Reverse();
+ ContainingTypes = builder.ToImmutable();
+ }
+
+ private static ContainingTypeInfo MakeContainingInfo(INamedTypeSymbol symbol)
+ {
+ var typeParams = symbol.TypeParameters;
+ var typeParamList = typeParams.Length > 0
+ ? "<" + string.Join(", ", typeParams.Select(p => p.Name)) + ">"
+ : string.Empty;
+ return new ContainingTypeInfo(symbol.Name, GetKeyword(symbol), typeParamList, typeParams.Length);
+ }
+
+ private static string GetKeyword(INamedTypeSymbol symbol)
+ {
+ if (symbol.IsRecord)
+ return symbol.IsValueType ? "record struct" : "record";
+
+ return symbol.TypeKind switch
+ {
+ TypeKind.Class => "class",
+ TypeKind.Struct => "struct",
+ TypeKind.Interface => "interface",
+ _ => "class",
+ };
+ }
+
+ public bool Equals(IdStructData other)
+ {
+ if (StructName != other.StructName) return false;
+ if (TypeParameters != other.TypeParameters) return false;
+ if (Arity != other.Arity) return false;
+ if (Namespace != other.Namespace) return false;
+ if (ContainingTypes.Length != other.ContainingTypes.Length) return false;
+
+ for (var i = 0; i < ContainingTypes.Length; i++)
+ {
+ if (!ContainingTypes[i].Equals(other.ContainingTypes[i])) return false;
+ }
+
+ return true;
+ }
+
+ public override bool Equals(object? obj) =>
+ obj is IdStructData other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = StructName.GetHashCode();
+ hash = (hash * 397) ^ TypeParameters.GetHashCode();
+ hash = (hash * 397) ^ Arity;
+ hash = (hash * 397) ^ (Namespace?.GetHashCode() ?? 0);
+
+ foreach (var ct in ContainingTypes)
+ hash = (hash * 397) ^ ct.GetHashCode();
+
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructDiagnostics.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructDiagnostics.cs
new file mode 100644
index 00000000..2859a587
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructDiagnostics.cs
@@ -0,0 +1,31 @@
+using Microsoft.CodeAnalysis;
+
+namespace Aspid.FastTools.Generators.IdStruct.Data;
+
+internal static class IdStructDiagnostics
+{
+ private const string Category = "Aspid.FastTools.IdStruct";
+
+ public static readonly DiagnosticDescriptor NotPartial = new(
+ id: "AFID001",
+ title: "IId struct must be partial",
+ messageFormat: "Struct '{0}' implements 'Aspid.FastTools.Ids.IId' but is not declared partial; the generator cannot emit the required '_id' field and 'Id' property",
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor MemberConflict = new(
+ id: "AFID002",
+ title: "Generated IId members already declared",
+ messageFormat: "Struct '{0}' already declares member(s) that the IId generator would emit: {1}",
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor? GetDescriptor(string id) => id switch
+ {
+ "AFID001" => NotPartial,
+ "AFID002" => MemberConflict,
+ _ => null,
+ };
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructResult.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructResult.cs
new file mode 100644
index 00000000..5f860954
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/Data/IdStructResult.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Aspid.FastTools.Generators.IdStruct.Data;
+
+internal readonly struct IdStructResult : IEquatable
+{
+ public readonly IdStructData? Data;
+ public readonly DiagnosticInfo? Diagnostic;
+
+ public IdStructResult(IdStructData? data, DiagnosticInfo? diagnostic)
+ {
+ Data = data;
+ Diagnostic = diagnostic;
+ }
+
+ public bool IsEmpty => Data is null && Diagnostic is null;
+
+ public bool Equals(IdStructResult other) =>
+ Nullable.Equals(Data, other.Data) && Nullable.Equals(Diagnostic, other.Diagnostic);
+
+ public override bool Equals(object? obj) => obj is IdStructResult other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = Data?.GetHashCode() ?? 0;
+ hash = (hash * 397) ^ (Diagnostic?.GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/IdStructGenerator.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/IdStructGenerator.cs
new file mode 100644
index 00000000..318a0571
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/IdStruct/IdStructGenerator.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Aspid.FastTools.Generators.IdStruct.Data;
+using Aspid.FastTools.Generators.IdStruct.Bodies;
+
+namespace Aspid.FastTools.Generators.IdStruct;
+
+[Generator(LanguageNames.CSharp)]
+internal sealed class IdStructGenerator : IIncrementalGenerator
+{
+ private const string IIdFullName = "Aspid.FastTools.Ids.IId";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.SyntaxProvider
+ .CreateSyntaxProvider(Predicate, Transform)
+ .Where(static r => !r.IsEmpty);
+
+ context.RegisterSourceOutput(provider, static (ctx, result) => Emit(ctx, result));
+ }
+
+ private static bool Predicate(SyntaxNode node, CancellationToken _)
+ {
+ if (node is not StructDeclarationSyntax structDecl) return false;
+ return structDecl.BaseList is { Types.Count: > 0 };
+ }
+
+ private static IdStructResult Transform(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ var structDecl = (StructDeclarationSyntax)context.Node;
+
+ if (context.SemanticModel.GetDeclaredSymbol(structDecl, ct) is not INamedTypeSymbol symbol)
+ return default;
+
+ var iidInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(IIdFullName);
+ if (iidInterface is null) return default;
+
+ var implementsIId = false;
+ foreach (var iface in symbol.AllInterfaces)
+ {
+ if (!SymbolEqualityComparer.Default.Equals(iface, iidInterface)) continue;
+ implementsIId = true;
+ break;
+ }
+
+ if (!implementsIId) return default;
+
+ if (!structDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
+ {
+ return new IdStructResult(
+ data: null,
+ diagnostic: new DiagnosticInfo(
+ IdStructDiagnostics.NotPartial,
+ structDecl.Identifier.GetLocation(),
+ symbol.Name));
+ }
+
+ var conflicts = CollectMemberConflicts(symbol);
+ if (conflicts.Count > 0)
+ {
+ return new IdStructResult(
+ data: null,
+ diagnostic: new DiagnosticInfo(
+ IdStructDiagnostics.MemberConflict,
+ structDecl.Identifier.GetLocation(),
+ symbol.Name,
+ string.Join(", ", conflicts)));
+ }
+
+ return new IdStructResult(new IdStructData(symbol), diagnostic: null);
+ }
+
+ private static List CollectMemberConflicts(INamedTypeSymbol symbol)
+ {
+ var result = new List();
+ AddIfDeclared(symbol, "_id", result);
+ AddIfDeclared(symbol, "Id", result);
+ AddIfDeclared(symbol, "__stringId", result);
+ return result;
+ }
+
+ private static void AddIfDeclared(INamedTypeSymbol symbol, string memberName, List output)
+ {
+ foreach (var member in symbol.GetMembers(memberName))
+ {
+ if (member.Kind is not (SymbolKind.Field or SymbolKind.Property)) continue;
+ output.Add(memberName);
+ return;
+ }
+ }
+
+ private static void Emit(SourceProductionContext context, IdStructResult result)
+ {
+ if (result.Diagnostic is { } diag)
+ context.ReportDiagnostic(diag.ToDiagnostic());
+
+ if (result.Data is { } data)
+ IdStructBody.GenerateCode(context, data);
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Bodies/ExtensionClassBody.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Bodies/ExtensionClassBody.cs
new file mode 100644
index 00000000..71818031
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Bodies/ExtensionClassBody.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Aspid.Generators.Helper;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Aspid.FastTools.Generators.ProfilerMarkers.Data;
+using static Aspid.Generators.Helper.Classes;
+using static Aspid.FastTools.Descriptions.General;
+using static Aspid.Generators.Helper.Unity.UnityClasses;
+
+namespace Aspid.FastTools.Generators.ProfilerMarkers.Bodies;
+
+internal static class ExtensionClassBody
+{
+ public static void GenerateCode(in SourceProductionContext context, in ImmutableArray markerCalls)
+ {
+ var typeGroups = markerCalls.GroupBy(call => call.Type.TypeKey);
+
+ foreach (var typeGroup in typeGroups)
+ GenerateForType(context, typeGroup);
+ }
+
+ private static void GenerateForType(SourceProductionContext context, IGrouping typeGroup)
+ {
+ var calls = typeGroup.ToImmutableArray();
+ var type = calls[0].Type;
+
+ var memberGroups = calls
+ .GroupBy(call => call.MethodKey)
+ .Select(g => g.OrderBy(c => c.Line).ToImmutableArray())
+ .ToImmutableArray();
+
+ var renamedByMember = AssignUniqueFieldNames(memberGroups);
+
+ var hasNamespace = !string.IsNullOrEmpty(type.Namespace);
+ var aritySuffix = type.Arity > 0 ? $"_{type.Arity}" : string.Empty;
+ var containingPart = type.ContainingTypeChain.Replace('.', '_');
+ var className = $"__{containingPart}{type.TypeName}{aritySuffix}ProfilerMarkerExtensions";
+ var isGeneric = type.TypeParamList.Length > 0;
+
+ var code = new CodeWriter()
+ .AppendLine("// ")
+ .AppendLine()
+ .AppendLineIf(hasNamespace, $"namespace {type.Namespace}")
+ .BeginBlockIf(hasNamespace)
+ .AppendLine($"[{ProfilerMarkerGeneratedCode}]")
+ .AppendLine($"internal static class {className}")
+ .BeginBlock()
+ .AppendProfilerMarkers(type, renamedByMember, isGeneric)
+ .AppendWithoutMessage(type, renamedByMember, isGeneric)
+ .EndBlock()
+ .EndBlockIf(hasNamespace);
+
+ var hintNamespacePart = hasNamespace ? type.Namespace + "." : string.Empty;
+ var hintName = $"{hintNamespacePart}{type.ContainingTypeChain}{type.TypeName}{aritySuffix}ProfilerMarkerExtensions.g.cs";
+ context.AddSource(hintName, code.GetSourceText());
+ }
+
+ private static List> AssignUniqueFieldNames(ImmutableArray> memberGroups)
+ {
+ var seen = new Dictionary();
+ var result = new List>(memberGroups.Length);
+
+ foreach (var member in memberGroups)
+ {
+ var renamed = ImmutableArray.CreateBuilder(member.Length);
+ foreach (var call in member)
+ {
+ seen.TryGetValue(call.MarkerName, out var count);
+ seen[call.MarkerName] = count + 1;
+ var fieldName = count == 0 ? call.MarkerName : $"{call.MarkerName}_{count + 1}";
+ renamed.Add(new RenamedCall(call, fieldName));
+ }
+ result.Add(renamed.MoveToImmutable());
+ }
+
+ return result;
+ }
+
+ private static CodeWriter AppendProfilerMarkers(
+ this CodeWriter code,
+ TypeData type,
+ List> members,
+ bool isGeneric)
+ {
+ if (isGeneric)
+ {
+ code.AppendLine($"private static class Markers{type.TypeParamList}{type.ConstraintsClause}")
+ .BeginBlock();
+ }
+
+ var fieldVisibility = isGeneric ? "public" : "private";
+
+ foreach (var member in members)
+ {
+ foreach (var renamed in member)
+ {
+ var markerValueExpression = BuildMarkerValueExpression(type, renamed.Call);
+ code.AppendMultiline(
+ $"""
+ [{ProfilerMarkerGeneratedCode}]
+ {fieldVisibility} static readonly {ProfilerMarker} {renamed.FieldName} = new({markerValueExpression});
+
+ """);
+ }
+ }
+
+ if (isGeneric)
+ code.EndBlock().AppendLine();
+
+ return code;
+ }
+
+ private static CodeWriter AppendWithoutMessage(
+ this CodeWriter code,
+ TypeData type,
+ List> members,
+ bool isGeneric)
+ {
+ code.AppendLine($"[{ProfilerMarkerGeneratedCode}]")
+ .AppendLine($"public static {ProfilerMarker}.AutoScope Marker{type.TypeParamList}(this {type.FullyQualifiedDisplay} _, [{CallerLineNumberAttribute}] int line = -1){type.ConstraintsClause}")
+ .BeginBlock()
+ .AppendLine("#if ENABLE_PROFILER");
+
+ var prefix = isGeneric ? $"Markers{type.TypeParamList}." : string.Empty;
+ foreach (var member in members)
+ {
+ foreach (var renamed in member)
+ code.AppendLine($"if (line is {renamed.Call.Line}) return {prefix}{renamed.FieldName}.Auto();");
+ }
+
+ code.AppendLine("#endif")
+ .AppendLine("return default;")
+ .EndBlock();
+
+ return code;
+ }
+
+ private static string BuildMarkerValueExpression(TypeData type, MarkerCall call)
+ {
+ if (type.Arity == 0)
+ return $"\"{type.TypeName}.{call.Label} ({call.Line})\"";
+
+ var typeArgs = ExtractTypeParamNames(type.TypeParamList);
+ var interpolations = string.Join(", ", typeArgs.Select(p => $"{{typeof({p}).Name}}"));
+ return $"$\"{type.TypeName}<{interpolations}>.{call.Label} ({call.Line})\"";
+ }
+
+ private static IEnumerable ExtractTypeParamNames(string typeParamList)
+ {
+ if (string.IsNullOrEmpty(typeParamList)) yield break;
+
+ var inner = typeParamList.Substring(1, typeParamList.Length - 2);
+ foreach (var part in inner.Split(','))
+ yield return part.Trim();
+ }
+
+ private readonly struct RenamedCall
+ {
+ public readonly MarkerCall Call;
+ public readonly string FieldName;
+
+ public RenamedCall(MarkerCall call, string fieldName)
+ {
+ Call = call;
+ FieldName = fieldName;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/MarkerCall.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/MarkerCall.cs
new file mode 100644
index 00000000..b645efcc
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/MarkerCall.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace Aspid.FastTools.Generators.ProfilerMarkers.Data;
+
+internal readonly struct MarkerCall : IEquatable
+{
+ public readonly TypeData Type;
+ public readonly string MethodKey;
+ public readonly int Line;
+ public readonly string MarkerName;
+ public readonly string Label;
+
+ public MarkerCall(
+ TypeData type,
+ string methodKey,
+ int line,
+ string markerName,
+ string markerValue)
+ {
+ Type = type;
+ MethodKey = methodKey;
+ Line = line;
+ MarkerName = markerName + "_Marker_Line_" + line;
+ Label = markerValue;
+ }
+
+ public bool Equals(MarkerCall other) =>
+ Type.Equals(other.Type)
+ && MethodKey == other.MethodKey
+ && Line == other.Line
+ && MarkerName == other.MarkerName
+ && Label == other.Label;
+
+ public override bool Equals(object? obj) => obj is MarkerCall other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = Type.GetHashCode();
+ hash = (hash * 397) ^ MethodKey.GetHashCode();
+ hash = (hash * 397) ^ Line;
+ hash = (hash * 397) ^ MarkerName.GetHashCode();
+ hash = (hash * 397) ^ Label.GetHashCode();
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/TypeData.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/TypeData.cs
new file mode 100644
index 00000000..669478a3
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/Data/TypeData.cs
@@ -0,0 +1,63 @@
+using System;
+
+namespace Aspid.FastTools.Generators.ProfilerMarkers.Data;
+
+internal readonly struct TypeData : IEquatable
+{
+ public readonly string TypeKey;
+ public readonly string TypeName;
+ public readonly string? Namespace;
+ public readonly string ContainingTypeChain;
+ public readonly string FullyQualifiedDisplay;
+ public readonly string TypeParamList;
+ public readonly string ConstraintsClause;
+ public readonly int Arity;
+
+ public TypeData(
+ string typeKey,
+ string typeName,
+ string? @namespace,
+ string containingTypeChain,
+ string fullyQualifiedDisplay,
+ string typeParamList,
+ string constraintsClause,
+ int arity)
+ {
+ TypeKey = typeKey;
+ TypeName = typeName;
+ Namespace = @namespace;
+ ContainingTypeChain = containingTypeChain;
+ FullyQualifiedDisplay = fullyQualifiedDisplay;
+ TypeParamList = typeParamList;
+ ConstraintsClause = constraintsClause;
+ Arity = arity;
+ }
+
+ public bool Equals(TypeData other) =>
+ TypeKey == other.TypeKey
+ && TypeName == other.TypeName
+ && Namespace == other.Namespace
+ && ContainingTypeChain == other.ContainingTypeChain
+ && FullyQualifiedDisplay == other.FullyQualifiedDisplay
+ && TypeParamList == other.TypeParamList
+ && ConstraintsClause == other.ConstraintsClause
+ && Arity == other.Arity;
+
+ public override bool Equals(object? obj) => obj is TypeData other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = TypeKey.GetHashCode();
+ hash = (hash * 397) ^ TypeName.GetHashCode();
+ hash = (hash * 397) ^ (Namespace?.GetHashCode() ?? 0);
+ hash = (hash * 397) ^ ContainingTypeChain.GetHashCode();
+ hash = (hash * 397) ^ FullyQualifiedDisplay.GetHashCode();
+ hash = (hash * 397) ^ TypeParamList.GetHashCode();
+ hash = (hash * 397) ^ ConstraintsClause.GetHashCode();
+ hash = (hash * 397) ^ Arity;
+ return hash;
+ }
+ }
+}
diff --git a/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/ProfilerMarkersGenerator.cs b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/ProfilerMarkersGenerator.cs
new file mode 100644
index 00000000..4e445eae
--- /dev/null
+++ b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Generators/ProfilerMarkers/ProfilerMarkersGenerator.cs
@@ -0,0 +1,233 @@
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Aspid.FastTools.Generators.ProfilerMarkers.Data;
+using Aspid.FastTools.Generators.ProfilerMarkers.Bodies;
+
+namespace Aspid.FastTools.Generators.ProfilerMarkers;
+
+[Generator(LanguageNames.CSharp)]
+internal sealed class ProfilerMarkersGenerator : IIncrementalGenerator
+{
+ private const string TargetClassName = "ProfilerMarkerExtensionsForGenerator";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var callsProvider = context.SyntaxProvider
+ .CreateSyntaxProvider(Predicate, Transform)
+ .Where(static markerCall => markerCall.HasValue)
+ .Select(static (markerCall, _) => markerCall!.Value);
+
+ var collected = callsProvider.Collect();
+ context.RegisterSourceOutput(collected, GenerateCode);
+ }
+
+ private static bool Predicate(SyntaxNode node, CancellationToken _)
+ {
+ if (node is not InvocationExpressionSyntax invocation) return false;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccessExpression) return false;
+
+ return memberAccessExpression.Name is IdentifierNameSyntax
+ {
+ Identifier.ValueText: "Marker"
+ };
+ }
+
+ private static MarkerCall? Transform(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ var node = context.Node;
+ if (node is not InvocationExpressionSyntax invocation) return null;
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccessExpression) return null;
+ if (memberAccessExpression.Name is not IdentifierNameSyntax idName || idName.Identifier.ValueText is not "Marker") return null;
+
+ // Semantic gate: only match Marker() declared on the global-namespace ProfilerMarkerExtensionsForGenerator class.
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation, ct);
+ if (symbolInfo.Symbol is not IMethodSymbol invokedMethod) return null;
+ var owningType = invokedMethod.ContainingType;
+ if (owningType is null) return null;
+ if (owningType.Name != TargetClassName) return null;
+ if (!owningType.ContainingNamespace.IsGlobalNamespace) return null;
+
+ var initialEnclosing = context.SemanticModel.GetEnclosingSymbol(invocation.SpanStart, ct);
+ if (ResolveEnclosingMember(initialEnclosing) is not { } enclosingInfo) return null;
+ var (namedTypeSymbol, markerName, methodKey) = enclosingInfo;
+
+ var markerValue = markerName;
+
+ // Walk past any parentheses so `(this.Marker()).WithName("x")` is still recognised.
+ SyntaxNode outer = invocation;
+ while (outer.Parent is ParenthesizedExpressionSyntax paren)
+ outer = paren;
+
+ if (outer.Parent is MemberAccessExpressionSyntax memberAccessExpressionWithName
+ && memberAccessExpressionWithName.Name is IdentifierNameSyntax { Identifier.ValueText: "WithName" }
+ && memberAccessExpressionWithName.Parent is InvocationExpressionSyntax invocationExpressionWithName
+ && invocationExpressionWithName.ArgumentList.Arguments.FirstOrDefault()?.Expression is { } argExpr
+ && TryExtractStringLiteral(argExpr) is { } extracted)
+ {
+ markerValue = extracted;
+ }
+
+ var lineSpan = invocation.GetLocation().GetLineSpan();
+ var lineNumber = lineSpan.StartLinePosition.Line + 1;
+
+ var typeData = BuildTypeData(namedTypeSymbol);
+
+ return new MarkerCall(typeData, methodKey, lineNumber, markerName, markerValue);
+ }
+
+ private static (INamedTypeSymbol Type, string MarkerName, string MethodKey)? ResolveEnclosingMember(ISymbol? enclosing)
+ {
+ // Walk past synthesized symbols (lambdas, local functions, anonymous methods)
+ // until we find a real declared member that owns the call site.
+ while (enclosing is not null)
+ {
+ switch (enclosing)
+ {
+ case IMethodSymbol method:
+ if (method.MethodKind is MethodKind.LambdaMethod
+ or MethodKind.AnonymousFunction
+ or MethodKind.LocalFunction)
+ {
+ enclosing = method.ContainingSymbol;
+ continue;
+ }
+ if (method.ContainingType is null) return null;
+ return (method.ContainingType, ResolveMarkerName(method), method.ToDisplayString());
+
+ case IFieldSymbol field:
+ if (field.ContainingType is null) return null;
+ return (field.ContainingType, field.Name, field.ToDisplayString());
+
+ case IPropertySymbol property:
+ if (property.ContainingType is null) return null;
+ return (property.ContainingType, property.Name, property.ToDisplayString());
+
+ default:
+ enclosing = enclosing.ContainingSymbol;
+ continue;
+ }
+ }
+
+ return null;
+ }
+
+ private static TypeData BuildTypeData(INamedTypeSymbol symbol)
+ {
+ var typeName = symbol.Name;
+ var ns = symbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : symbol.ContainingNamespace.ToDisplayString();
+
+ var containingChain = string.Empty;
+ if (symbol.ContainingType is not null)
+ {
+ var stack = new Stack();
+ for (var t = symbol.ContainingType; t is not null; t = t.ContainingType)
+ stack.Push(t.Name);
+
+ var sb = new System.Text.StringBuilder();
+ foreach (var name in stack)
+ sb.Append(name).Append('.');
+ containingChain = sb.ToString();
+ }
+
+ var typeKey = (ns is null ? string.Empty : ns + ".") + containingChain + typeName;
+
+ var typeParameters = symbol.TypeParameters;
+ var isGeneric = typeParameters.Length > 0;
+ var typeParamList = isGeneric
+ ? "<" + string.Join(", ", typeParameters.Select(p => p.Name)) + ">"
+ : string.Empty;
+ var constraintsClause = BuildConstraintsClause(typeParameters);
+
+ var fullyQualifiedDisplay = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ return new TypeData(
+ typeKey: typeKey,
+ typeName: typeName,
+ @namespace: ns,
+ containingTypeChain: containingChain,
+ fullyQualifiedDisplay: fullyQualifiedDisplay,
+ typeParamList: typeParamList,
+ constraintsClause: constraintsClause,
+ arity: symbol.Arity);
+ }
+
+ private static string? TryExtractStringLiteral(ExpressionSyntax expr)
+ {
+ switch (expr)
+ {
+ case LiteralExpressionSyntax lit when lit.Token.IsKind(SyntaxKind.StringLiteralToken):
+ return lit.Token.ValueText;
+
+ case InterpolatedStringExpressionSyntax interp:
+ {
+ var sb = new StringBuilder();
+ foreach (var content in interp.Contents)
+ {
+ if (content is not InterpolatedStringTextSyntax text) return null;
+ sb.Append(text.TextToken.ValueText);
+ }
+ return sb.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private static string ResolveMarkerName(IMethodSymbol enclosing)
+ {
+ if (enclosing.AssociatedSymbol is IPropertySymbol property)
+ {
+ return property.ExplicitInterfaceImplementations.Length > 0
+ ? property.ExplicitInterfaceImplementations[0].Name
+ : property.Name;
+ }
+
+ if (enclosing.MethodKind is MethodKind.Constructor)
+ return "Ctor";
+
+ return enclosing.ExplicitInterfaceImplementations.Length > 0
+ ? enclosing.ExplicitInterfaceImplementations[0].Name
+ : enclosing.Name;
+ }
+
+ private static string BuildConstraintsClause(ImmutableArray typeParameters)
+ {
+ if (typeParameters.Length is 0) return string.Empty;
+
+ var clauses = new List();
+ foreach (var tp in typeParameters)
+ {
+ var constraints = new List();
+
+ if (tp.HasReferenceTypeConstraint) constraints.Add("class");
+ else if (tp.HasUnmanagedTypeConstraint) constraints.Add("unmanaged");
+ else if (tp.HasValueTypeConstraint) constraints.Add("struct");
+
+ if (tp.HasNotNullConstraint) constraints.Add("notnull");
+
+ foreach (var ct in tp.ConstraintTypes)
+ constraints.Add(ct.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+
+ if (tp.HasConstructorConstraint) constraints.Add("new()");
+
+ if (constraints.Count > 0)
+ clauses.Add($"where {tp.Name} : {string.Join(", ", constraints)}");
+ }
+
+ return clauses.Count is 0 ? string.Empty : " " + string.Join(" ", clauses);
+ }
+
+ private static void GenerateCode(SourceProductionContext context, ImmutableArray markerCalls)
+ {
+ if (markerCalls.Length is 0) return;
+ ExtensionClassBody.GenerateCode(context, markerCalls);
+ }
+}
diff --git a/Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators/Properties/launchSettings.json b/Aspid.FastTools.Generators/Aspid.FastTools.Generators/Properties/launchSettings.json
similarity index 100%
rename from Aspid.UnityFastTools.Generators/UnityFastToolsGenerators/Aspid.UnityFastTools.Generators/Properties/launchSettings.json
rename to Aspid.FastTools.Generators/Aspid.FastTools.Generators/Properties/launchSettings.json
diff --git a/Aspid.FastTools.Generators/CLAUDE.md b/Aspid.FastTools.Generators/CLAUDE.md
new file mode 100644
index 00000000..dc9806e2
--- /dev/null
+++ b/Aspid.FastTools.Generators/CLAUDE.md
@@ -0,0 +1,219 @@
+# Aspid.FastTools.Generators
+
+Standalone .NET solution containing Roslyn source generators for the `com.aspid.fasttools` Unity package.
+
+## Commands
+
+```bash
+# Build and auto-deploy DLL into Unity package
+dotnet build -c Release
+
+# Run generator unit tests
+dotnet test
+```
+
+`Directory.Build.targets` copies the compiled DLL to `../Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll` on build.
+
+A repo-level PostToolUse hook (`.claude/hooks/rebuild-generators-on-change.sh`) also runs `dotnet build` automatically after any `Edit`/`Write` to `*.cs` under `Aspid.FastTools.Generators/Aspid.FastTools.Generators/`. The hook intentionally **does not** trigger for tests, the Sample project, or Unity-side edits — keep that scope if you modify it.
+
+## Solution Structure
+
+```
+Aspid.FastTools.Generators/ ← generator implementation
+Aspid.FastTools.Generators.Tests/ ← unit tests + GeneratorTestHost helper
+Aspid.FastTools.Generators.Sample/ ← manual smoke-test project
+Aspid.FastTools.Generators.sln
+Directory.Build.targets
+```
+
+## Target Framework
+
+`netstandard2.0` — required by Roslyn. No Unity assemblies, no runtime packages.
+
+## Dependencies
+
+| Package | Role |
+|---|---|
+| `Microsoft.CodeAnalysis.CSharp` 4.3.0 | Roslyn semantic model and syntax |
+| `Aspid.Generators.Helper` | `CodeWriter` utility for emitting source |
+| `Aspid.Generators.Helper.Unity` | Unity-specific analysis helpers |
+| `SourceGenerator.Foundations` 2.0.13 | Incremental generator infrastructure |
+
+## Generator Implementation Pattern
+
+All generators implement `IIncrementalGenerator` (never the deprecated `ISourceGenerator`).
+
+### Three-Stage Pipeline
+
+```
+Predicate (SyntaxNode) → Transform (SemanticModel) → GenerateCode (SourceProductionContext)
+```
+
+**1. Predicate** — cheap syntax-only filter, no semantic model:
+```csharp
+private static bool Predicate(SyntaxNode node, CancellationToken _)
+{
+ if (node is not StructDeclarationSyntax s) return false;
+ return s.BaseList is { Types.Count: > 0 };
+}
+```
+
+Keep predicates broad enough that diagnostics for malformed declarations (e.g. `IId` struct without `partial`) are still reachable in Transform — they need the semantic model to decide whether to emit a diagnostic vs. silently skip.
+
+**2. Transform** — semantic extraction. Returns a value-equatable `readonly struct` (data and/or diagnostic) or `default` to skip:
+```csharp
+private static IdStructResult Transform(GeneratorSyntaxContext ctx, CancellationToken ct)
+{
+ var symbol = ctx.SemanticModel.GetDeclaredSymbol(structDecl, ct) as INamedTypeSymbol;
+ // resolve IId by metadata name, check AllInterfaces
+ // emit diagnostic if precondition fails, otherwise wrap data
+ return new IdStructResult(new IdStructData(symbol), diagnostic: null);
+}
+```
+
+Always pass `CancellationToken` through to semantic-model APIs that accept it (`GetDeclaredSymbol`, `GetSymbolInfo`, `GetEnclosingSymbol`).
+
+**3. GenerateCode** — emit source using `CodeWriter`, report diagnostics:
+```csharp
+private static void Emit(SourceProductionContext context, IdStructResult result)
+{
+ if (result.Diagnostic is { } d) context.ReportDiagnostic(d.ToDiagnostic());
+ if (result.Data is { } data) IdStructBody.GenerateCode(context, data);
+}
+```
+
+### Data Structures — value-equality is mandatory
+
+Pipeline data passed between stages **must be value-equatable**. Roslyn caches results by comparing them with `Equals`; if equality is reference-based (the default for `readonly struct` containing reference fields), the cache never hits and the generator re-runs on every keystroke. Worse, holding `ISymbol` keeps the source `Compilation` alive.
+
+**Forbidden in data structs:** any `ISymbol`/`INamedTypeSymbol`/`IMethodSymbol`/`SyntaxNode` field. Extract everything you need at Transform time as primitives/strings.
+
+**Required for every data struct:**
+- `readonly struct` with explicit `IEquatable` implementation
+- `Equals` and `GetHashCode` over every field
+- For nested arrays use `ImmutableArray` with element-wise comparison (or `EquatableArray` if introduced)
+
+Reference shapes:
+
+```csharp
+internal readonly struct TypeData : IEquatable
+{
+ public readonly string TypeKey; // "Foo.Outer.Inner"
+ public readonly string TypeName;
+ public readonly string? Namespace;
+ public readonly string ContainingTypeChain; // "Outer.Middle." or ""
+ public readonly string FullyQualifiedDisplay;
+ public readonly string TypeParamList; // "" or ""
+ public readonly string ConstraintsClause;
+ public readonly int Arity;
+ // ... ctor + IEquatable Equals/GetHashCode over all fields
+}
+
+internal readonly struct IdStructData : IEquatable
+{
+ public readonly string StructName;
+ public readonly string TypeParameters; // "" or ""
+ public readonly int Arity;
+ public readonly string? Namespace;
+ public readonly ImmutableArray ContainingTypes;
+ // ... IEquatable walks ContainingTypes element-wise
+}
+```
+
+Cache stability is regression-tested in `IncrementalCacheTests` — that test runs each generator twice over compilations differing only in an unrelated source file, and asserts every output step is `Cached`/`Unchanged`. Adding a non-equatable field to any pipeline struct will fail it.
+
+### Diagnostic Delivery
+
+Generators that need to report diagnostics use a value-equatable wrapper that survives the pipeline:
+
+```csharp
+internal readonly struct DiagnosticInfo : IEquatable
+{
+ public readonly string DescriptorId; // "AFID001"
+ public readonly string MessageArg0;
+ public readonly string? MessageArg1;
+ public readonly string? FilePath; // location is rebuilt from path + spans
+ public readonly TextSpan TextSpan;
+ public readonly LinePositionSpan LineSpan;
+
+ public Diagnostic ToDiagnostic() { ... } // looks up the DiagnosticDescriptor by id
+}
+
+internal readonly struct IdStructResult : IEquatable
+{
+ public readonly IdStructData? Data;
+ public readonly DiagnosticInfo? Diagnostic;
+ public bool IsEmpty => Data is null && Diagnostic is null;
+}
+```
+
+Transform returns the wrapper; the `RegisterSourceOutput` callback emits the diagnostic and/or generates the source. Storing `Location` directly in the wrapper would defeat caching (it holds a `SyntaxTree` reference); rebuild it from `FilePath` + spans inside `ToDiagnostic()`.
+
+Descriptors live in `Generators/{Feature}/Data/{Feature}Diagnostics.cs` with IDs prefixed `AFID` (Aspid FastTools, IdStruct domain). The csproj suppresses `RS2008` (analyzer release tracking) — this is a private in-tree generator, not a published analyzer package.
+
+### Generated File Naming
+
+Every hint name must include enough qualifiers to be unique across types with the same short name in different namespaces or containing types. Prefer `{ns}.{containing-chain}{Name}{aritySuffix}.{tag}.g.cs`.
+
+| Generator | Pattern |
+|---|---|
+| `IdStructGenerator` | `{Namespace}.{Containing.Chain.}{StructName}{_Arity?}.IId.g.cs` |
+| `ProfilerMarkersGenerator` | `{Namespace}.{Containing.Chain.}{TypeName}{_Arity?}ProfilerMarkerExtensions.g.cs` |
+
+`{_Arity?}` is `_2`, `_3`, … for generic types (omitted when arity is 0). The containing chain is dot-joined with a trailing dot, or empty for top-level types.
+
+The emitted `partial`/extension class names follow the same uniqueness rule; for `ProfilerMarkersGenerator` the extension class is `__{Containing_}{TypeName}{_Arity?}ProfilerMarkerExtensions` (containing chain joined with `_`).
+
+All generated files begin with `// `.
+
+## Existing Generators
+
+### ProfilerMarkersGenerator
+
+Finds every `.Marker()` call site, semantically verifies it resolves to `ProfilerMarkerExtensionsForGenerator.Marker` (global-namespace class on the Unity side — calls to user-defined extensions named `Marker` are ignored), and generates `private static readonly ProfilerMarker` fields unique to each call site.
+
+- **Field name:** `"{markerName}_Marker_Line_{line}"`. If multiple `.Marker()` calls share the same `(markerName, line)` (e.g. two calls on one source line), subsequent fields receive a `_2`/`_3` suffix and the dispatcher is updated in lockstep.
+- **Marker display value:** `"{TypeName}.{member} ({line})"`. For generic enclosing types the marker value is interpolated with `typeof(T).Name` so each closed instantiation gets its own runtime label.
+- **`.WithName(string)` override:** accepts string literals, plain interpolated strings without holes (`$"X"`), and survives a `(this.Marker()).WithName(...)` parenthesised receiver. Interpolated strings with substitutions are silently ignored — the generator falls back to the method name.
+- **Enclosing resolution:** walks past lambdas, anonymous functions, and local functions to the nearest declared `IMethodSymbol`/`IFieldSymbol`/`IPropertySymbol`. Field initializers use the field name as the marker.
+- **Release-build gating:** the dispatcher body (the `if (line is N) return …` chain) is wrapped in `#if ENABLE_PROFILER`. When the symbol is undefined (non-development player builds) the method falls through to `return default;`, so player builds pay no per-call lookup cost. The static `ProfilerMarker` field declarations are emitted unconditionally — their `Begin`/`End` calls already strip via Unity's `[Conditional("ENABLE_PROFILER")]`.
+
+### IdStructGenerator
+
+Finds `partial struct` types implementing `Aspid.FastTools.Ids.IId` (transitive interfaces are resolved through `INamedTypeSymbol.AllInterfaces`) and generates boilerplate: serialized `_id` field, `Id` property, editor-only `__stringId` field. All generated members carry `[GeneratedCode("Aspid.FastTools.Generators.IdStructGenerator", "1.0.0")]`.
+
+**Supported shapes:**
+- Nested types — generated code is wrapped in matching `partial class`/`partial struct`/`partial record`/`partial record struct` containing-type declarations, with full type-parameter lists (`partial class Outer`).
+- Generic target structs — `partial struct MyId : IId` is supported; the wrapper is emitted with `` and the hint name encodes the arity.
+- Global namespace, file-scoped namespace, multi-level nesting, transitive `IId` implementations.
+
+**Diagnostics:**
+
+| Id | Title | Trigger |
+|---|---|---|
+| `AFID001` | IId struct must be partial | Struct implements `IId` but the declaration lacks `partial` |
+| `AFID002` | Generated IId members already declared | User declares any of `_id`/`Id`/`__stringId` themselves |
+
+When a diagnostic fires, the body is **not** emitted — the user gets a generator-level error pointing at the struct identifier instead of a CS compile error from inside the generated source.
+
+## Conventions
+
+- `[Generator(LanguageNames.CSharp)]` on every generator class
+- Generators and their `Bodies/Data/` types are `internal sealed` / `internal`. The Tests project gets access via `` in the csproj
+- Generated members use the attribute string from `Descriptions/General.cs` (`ProfilerMarkerGeneratedCode` for ProfilerMarkers, `IdStructGeneratedCode` for IdStruct)
+- Always check `IsGlobalNamespace` before emitting a `namespace` block
+- Avoid LINQ in hot Predicate/Transform paths — allocations defeat incremental caching
+- Diagnostic descriptor IDs use the `AFID` prefix (Aspid FastTools IdStruct); add new descriptor groups under their feature's `Data/` folder
+
+## Test Infrastructure
+
+`Aspid.FastTools.Generators.Tests/Helpers/GeneratorTestHost.cs` provides:
+
+- `RunIdStruct(userSource)` / `RunProfilerMarkers(userSource)` — drive the generator over a synthesised compilation that includes the necessary stubs (`IIdDefinition`, `UnityEngineStubs`, `ProfilerMarkerStubs`)
+- `AssertNoErrors(GeneratorRun)` — fails the test if either the generator emitted error-severity diagnostics **or** the resulting `Compilation.GetDiagnostics()` reports any compile error in the generated source
+
+Use `AssertNoErrors` on every test that exercises a happy-path emission — it catches malformed C# (missing using directives, bad escapes, unbalanced blocks) for free.
+
+`IncrementalCacheTests` verifies cache stability — when extending pipeline data, run it to confirm equality is intact.
+
+The `Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit` package is referenced but not currently used; the harness uses `CSharpGeneratorDriver` directly because the test surface is small.
diff --git a/Aspid.FastTools.Generators/Directory.Build.targets b/Aspid.FastTools.Generators/Directory.Build.targets
new file mode 100644
index 00000000..f176b584
--- /dev/null
+++ b/Aspid.FastTools.Generators/Directory.Build.targets
@@ -0,0 +1,11 @@
+
+
+
+
+ <_UnityDestination>$(MSBuildThisFileDirectory)../Aspid.FastTools/Assets/Plugins/Aspid/FastTools/
+
+
+
+
+
+
diff --git a/Aspid.UnityFastTools/.gitignore b/Aspid.FastTools/.gitignore
similarity index 100%
rename from Aspid.UnityFastTools/.gitignore
rename to Aspid.FastTools/.gitignore
diff --git a/Aspid.UnityFastTools/Assets/Plugins.meta b/Aspid.FastTools/Assets/Plugins.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins.meta
rename to Aspid.FastTools/Assets/Plugins.meta
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid.meta b/Aspid.FastTools/Assets/Plugins/Aspid.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid.meta
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools.meta
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Aspid.UnityFastTools.Generators.dll b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll
similarity index 99%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Aspid.UnityFastTools.Generators.dll
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll
index 95a75d75..54cbb84b 100644
Binary files a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Aspid.UnityFastTools.Generators.dll and b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll differ
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Aspid.UnityFastTools.Generators.dll.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Aspid.UnityFastTools.Generators.dll.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Aspid.FastTools.Generators.dll.meta
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN.meta
new file mode 100644
index 00000000..cc63ccc8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 227c6c85a08c428c8299094196de787b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/README.md
new file mode 100644
index 00000000..8d26cc44
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/README.md
@@ -0,0 +1,535 @@
+# Aspid.FastTools
+
+**Aspid.FastTools** is a set of tools designed to minimize routine code writing in Unity.
+
+## Source Code
+
+[[Aspid.FastTools](https://github.com/VPDPersonal/Aspid.FastTools)]
+
+
+---
+
+## Integration
+
+Install Aspid.FastTools using one of the following methods:
+
+- **Download .unitypackage** — Visit the [Release page on GitHub](https://github.com/VPDPersonal/Aspid.FastTools/releases) and download the latest version, `Aspid.FastTools.X.X.X.unitypackage`. Import it into your project.
+- **Via UPM** (Unity Package Manager) integrate the following package:
+ - `https://github.com/VPDPersonal/Aspid.FastTools.git?path=Aspid.FastTools/Assets/Plugins/Aspid/FastTools`
+
+---
+
+## Donate
+
+This project is developed on a voluntary basis. If you find it useful, you can support its development financially. This helps allocate more time to improving and maintaining **Aspid.FastTools**.
+
+You can donate via the following platforms:
+* \[[Unity Asset Store](https://assetstore.unity.com/packages/slug/365584)\]
+
+---
+
+## Namespaces
+
+| Namespace | Description |
+|-----------|-------------|
+| `Aspid.FastTools.Types` | `SerializableType`, `SerializableType`, `ComponentTypeSelector`, `TypeSelectorAttribute` |
+| `Aspid.FastTools.Enums` | `EnumValues` |
+| `Aspid.FastTools.Ids` | `IId`, `UniqueIdAttribute`, `IdRegistry` |
+| `Aspid.FastTools.UIElements` | Runtime `VisualElement` fluent extensions |
+| `Aspid.FastTools.Editors` | Editor helpers — `SerializedProperty` extensions, IMGUI scopes, `GetScriptName` |
+| `Aspid.FastTools.Types.Editors` · `.Enums.Editors` · `.Ids.Editors` · `.UIElements.Editors` | Per-feature editor code (property drawers, registry inspector, editor-only `VisualElement` extensions) |
+
+---
+
+## ProfilerMarker
+
+Provides source-generated `ProfilerMarker` registration. The generator creates a static marker per call-site, identified by the calling method and line number.
+
+```csharp
+using UnityEngine;
+
+public class MyBehaviour : MonoBehaviour
+{
+ private void Update()
+ {
+ DoSomething1();
+ DoSomething2();
+ }
+
+ private void DoSomething1()
+ {
+ using var _ = this.Marker();
+ // Some code
+ }
+
+ private void DoSomething2()
+ {
+ using (this.Marker())
+ {
+ // Some code
+ using var _ = this.Marker().WithName("Calculate");
+ // Some code
+ }
+ }
+}
+```
+
+### Generated code
+
+```csharp
+using Unity.Profiling;
+using System.Runtime.CompilerServices;
+
+internal static class __MyBehaviourProfilerMarkerExtensions
+{
+ private static readonly ProfilerMarker DoSomething1_Marker_Line_13 = new("MyBehaviour.DoSomething1 (13)");
+ private static readonly ProfilerMarker DoSomething2_Marker_Line_19 = new("MyBehaviour.DoSomething2 (19)");
+ private static readonly ProfilerMarker DoSomething2_Marker_Line_22 = new("MyBehaviour.Calculate (22)");
+
+ public static ProfilerMarker.AutoScope Marker(this MyBehaviour _, [CallerLineNumberAttribute] int line = -1)
+ {
+#if ENABLE_PROFILER
+ if (line is 13) return DoSomething1_Marker_Line_13.Auto();
+ if (line is 19) return DoSomething2_Marker_Line_19.Auto();
+ if (line is 22) return DoSomething2_Marker_Line_22.Auto();
+#endif
+ return default;
+ }
+}
+```
+
+### Result
+
+
+
+---
+
+## Serializable Type System
+
+Allows serializing a `System.Type` reference in the Unity Inspector. The selected type is stored as an assembly-qualified name and resolved lazily on first access.
+
+### SerializableType
+
+Two variants are available:
+
+- **`SerializableType`** — stores any type (base type is `object`)
+- **`SerializableType`** — stores a type constrained to `T` or its subclasses
+
+Both support implicit conversion to `System.Type`.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public class MyBehaviour : MonoBehaviour
+{
+ [SerializeField] private SerializableType _anyType;
+ [SerializeField] private SerializableType _behaviourType;
+
+ private void Start()
+ {
+ Type type1 = _anyType; // implicit operator
+ Type type2 = _behaviourType.Type; // explicit property
+
+ var instance = (MonoBehaviour)gameObject.AddComponent(type2);
+ }
+}
+```
+
+### ComponentTypeSelector
+
+A serializable struct that renders a type-switching dropdown in the Inspector. Add it as a field to a base class — picking a subtype rewrites `m_Script` on the `SerializedObject`, effectively changing the component or ScriptableObject to the chosen subtype.
+
+The dropdown is automatically constrained to subtypes of the class that declares the field. No additional configuration is required.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public abstract class BaseEnemy : MonoBehaviour
+{
+ [SerializeField] private ComponentTypeSelector _typeSelector;
+}
+
+public class FastEnemy : BaseEnemy { }
+public class TankEnemy : BaseEnemy { }
+```
+
+---
+
+### TypeSelectorAttribute
+
+An editor-only `PropertyAttribute` that restricts the type selection popup to specific base types. Applied to `string` fields that store assembly-qualified type names.
+
+```csharp
+[Conditional("UNITY_EDITOR")]
+public sealed class TypeSelectorAttribute : PropertyAttribute
+{
+ public TypeSelectorAttribute() // base type: object
+ public TypeSelectorAttribute(Type type)
+ public TypeSelectorAttribute(params Type[] types)
+ public TypeSelectorAttribute(string assemblyQualifiedName)
+ public TypeSelectorAttribute(params string[] assemblyQualifiedNames)
+
+ public TypeAllow Allow { get; set; } // default: TypeAllow.None
+}
+
+[Flags]
+public enum TypeAllow
+{
+ None = 0,
+ Abstract = 1,
+ Interface = 2,
+ All = Abstract | Interface
+}
+```
+
+| Property | Description |
+|----------|-------------|
+| `Allow` | Which special type categories (abstract classes, interfaces) the picker includes in addition to plain concrete classes. Default: `TypeAllow.None` |
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public class MyBehaviour : MonoBehaviour
+{
+ [TypeSelector(typeof(IMyInterface))]
+ [SerializeField] private string _typeName;
+
+ // Include abstract types and interfaces in the picker
+ [TypeSelector(typeof(object), Allow = TypeAllow.All)]
+ [SerializeField] private string _anyType;
+}
+```
+
+### Type Selector Window
+
+The Inspector shows a button that opens a searchable popup window with:
+
+- Hierarchical namespace organization
+- Text search with filtering
+- Keyboard navigation (Arrow keys, Enter, Escape)
+- Navigation history (back button)
+- Assembly disambiguation for types with identical names
+
+
+---
+
+## Enum System
+
+Provides serializable enum-to-value mappings configurable from the Inspector.
+
+### EnumValues\
+
+A serializable collection of `EnumValue` entries with a configurable default value. Implements `IEnumerable>`.
+
+```csharp
+[Serializable]
+public sealed class EnumValues : IEnumerable>
+```
+
+| Member | Description |
+|--------|-------------|
+| `TValue GetValue(Enum enumValue)` | Returns the mapped value, or `_defaultValue` if not found |
+| `bool Equals(Enum, Enum)` | Equality check with proper `[Flags]` support |
+
+Supports `[Flags]` enums: `Equals` uses `HasFlag` and treats `0`-valued members correctly.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Enums;
+
+public enum Direction { Left, Right, Up, Down }
+
+public class MyBehaviour : MonoBehaviour
+{
+ [SerializeField] private EnumValues _directionSprites;
+
+ private void SetIcon(Direction dir)
+ {
+ var sprite = _directionSprites.GetValue(dir);
+ _image.sprite = sprite;
+ }
+}
+```
+
+In the Inspector, select the enum type in the `EnumValues` header, then assign a value for each enum member. Right-click the property to open a context menu with **Populate Missing Enum Members** — it appends an entry for every enum member not yet in the list, seeded with the current Default Value.
+
+---
+
+## ID System
+
+> **Beta:** the ID System is currently in beta. The public API, generated code layout and editor workflow may change in future releases.
+
+Maps an asset-assignable name to a stable integer ID. Use the resulting `int` in `switch` statements and `Dictionary` keys without paying for string lookups at runtime.
+
+A single `IdRegistry` ScriptableObject maps string names to stable integer IDs and provides full `int ↔ string` lookups at runtime.
+
+### Setup
+
+**1.** Declare a `partial struct` implementing `IId`. The source generator adds the required fields and property automatically:
+
+```csharp
+using Aspid.FastTools.Ids;
+
+public partial struct EnemyId : IId { }
+```
+
+Generated code:
+
+```csharp
+public partial struct EnemyId
+{
+ [SerializeField] private string __stringId; // editor-only field, stripped from player builds
+ [SerializeField] private int _id;
+
+ public int Id => _id;
+}
+```
+
+The generator reports `AFID001` if the struct is missing `partial`, and `AFID002` if your code already declares `_id`, `Id`, or `__stringId` (the generator skips emission so you get a clear error pointing at the struct rather than a CS compile error inside generated source). Generic targets (`EnemyId`) and generic containing types are supported.
+
+**2.** Create the registry asset and bind it to the struct type in its Inspector:
+- `Assets → Create → Aspid → Id Registry`
+
+**3.** Use the struct as a serialized field. The Inspector shows a dropdown of registered names; the selector window also lets you create new entries on the fly:
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Ids;
+
+[CreateAssetMenu]
+public class EnemyDefinition : ScriptableObject
+{
+ [UniqueId] [SerializeField] private EnemyId _id;
+}
+```
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Ids;
+
+public class EnemySpawner : MonoBehaviour
+{
+ [SerializeField] private EnemyId _targetEnemy;
+
+ private void Spawn()
+ {
+ int id = _targetEnemy.Id; // stable integer, safe for switch / Dictionary
+ }
+}
+```
+
+### UniqueIdAttribute
+
+Marks a field as requiring a unique value across all assets of the declaring type. The Inspector shows a warning if two assets share the same ID.
+
+```csharp
+[Conditional("UNITY_EDITOR")]
+public sealed class UniqueIdAttribute : PropertyAttribute { }
+```
+
+### IdRegistry
+
+`ScriptableObject` in `Aspid.FastTools.Ids` that stores `(int, string)` entries and keeps the lookup tables available at runtime. Each name is assigned a stable, auto-incrementing ID that never changes when other entries are added or removed.
+
+| Member | Description |
+|--------|-------------|
+| `bool TryGetId(string name, out int id)` | Returns `true` and the ID when found; otherwise `false` |
+| `bool TryGetName(int id, out string name)` | Returns `true` and the name when found; otherwise `false` and `string.Empty` |
+| `bool Contains(int id)` | Whether an ID is registered |
+| `bool Contains(string name)` | Whether a name is registered |
+| `int Count` | Number of entries |
+| `IReadOnlyList Ids` · `IReadOnlyList IdNames` | Registered IDs / names, in registration order |
+| `IEnumerator> GetEnumerator()` | Iterate `(id, name)` pairs |
+
+The registry derives from `ScriptableObject` directly and exposes a generic counterpart `IdRegistry` (with `T : struct, IId`) that adds typed `Contains(T)` and `TryGetName(T, out string)` overloads. Edits — adding, renaming, removing entries — happen through the registry inspector and `RegistryEditorCore`, not via a public runtime API.
+
+---
+
+## SerializedProperty Extensions
+
+Chainable extensions on `SerializedProperty` for synchronizing the owning `SerializedObject`, writing typed values, and reflecting on the underlying field.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+```csharp
+property
+ .Update()
+ .SetVector3(Vector3.up)
+ .SetBool(true)
+ .ApplyModifiedProperties();
+```
+
+The package covers:
+
+- **Update / Apply** — `Update`, `UpdateIfRequiredOrScript`, `ApplyModifiedProperties`.
+- **Typed setters** — `SetValue` (generic dispatch) and `SetXxx` for `int`/`uint`/`long`/`ulong`/`float`/`double`/`bool`/`string`/`Color`/`Gradient`/`Hash128`/`Rect`/`RectInt`/`Bounds`/`BoundsInt`/`Vector2..4` (and `Vector2/3Int`)/`Quaternion`/`AnimationCurve`/`EntityId` (Unity 6.2+). Each comes with a paired `SetXxxAndApply` variant.
+- **Enum setters** — `SetEnumFlag` and `SetEnumIndex` (each + `AndApply`).
+- **Arrays** — `SetArraySize`, `AddArraySize`, `RemoveArraySize` (each + `AndApply`).
+- **References** — `SetManagedReference`, `SetObjectReference`, `SetExposedReference`, and `SetBoxed` (Unity 6+).
+- **Reflection helpers** — `GetPropertyType`, `GetMemberInfo`, `GetClassInstance` for resolving the C# member and runtime instance behind a property.
+
+> Full method-by-method reference: [SerializedPropertyExtensions.md](SerializedPropertyExtensions.md)
+
+---
+
+## IMGUI Layout Scopes
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+Three `ref struct` scopes — `VerticalScope`, `HorizontalScope`, `ScrollViewScope` — wrap `EditorGUILayout.Begin*` / `End*`. Each exposes a `Rect` property and calls the matching `End*` method on `Dispose`:
+
+```csharp
+using (VerticalScope.Begin())
+{
+ EditorGUILayout.LabelField("Item 1");
+ EditorGUILayout.LabelField("Item 2");
+}
+
+using (HorizontalScope.Begin())
+{
+ EditorGUILayout.LabelField("Left");
+ EditorGUILayout.LabelField("Right");
+}
+
+var scrollPos = Vector2.zero;
+using (ScrollViewScope.Begin(ref scrollPos))
+{
+ EditorGUILayout.LabelField("Scrollable content");
+}
+```
+
+Capture the group rect with the `out`-overload when needed:
+
+```csharp
+using (VerticalScope.Begin(out var rect, GUI.skin.box))
+{
+ EditorGUI.DrawRect(rect, new Color(0, 0, 0, 0.1f));
+ EditorGUILayout.LabelField("Boxed content");
+}
+```
+
+All `Begin` overloads match the corresponding `EditorGUILayout.Begin*` signatures (optional `GUIStyle`, `GUILayoutOption[]`, scroll view options, etc.).
+
+---
+
+## VisualElement Extensions
+
+Fluent extension methods for building UIToolkit trees in code. All methods return `T` (the element itself) for chaining.
+
+```csharp
+using Aspid.FastTools.UIElements; // runtime extensions
+using Aspid.FastTools.UIElements.Editors; // editor-only extensions (e.g. AddOpenScriptCommand)
+```
+
+### Quick reference
+
+The package covers:
+
+- **Core element operations** — name, visibility, tooltip, user data, picking mode, data source, and `AddChild`/`InsertChild` helpers.
+- **Focus** — `SetFocus`, `SetBlur`, `SetTabIndex`, `SetFocusable`.
+- **USS** — `AddClass`/`RemoveClass`/`ToggleInClass`/`EnableInClass`, `AddStyleSheets[FromResource]`.
+- **Styles** — every `IStyle` property: layout, size, spacing, font, text, color, border, background, transform (incl. Unity 6.3+ aspect/filter/material), transition, overflow, slice, cursor.
+- **Specialized elements** — `TextElement`, `ITextEdition`, `ITextSelection`, `BaseField`, `BaseBoolField` (Toggle), `INotifyValueChanged` (with optional `Unity.Mathematics` types), `IMixedValueSupport`, `Button`, `Slider`/`BaseSlider`, `ProgressBar`, `HelpBox`, `Foldout`, `Image`, `IMGUIContainer`, plus the full `ListView`/`TreeView`/`MultiColumn*` surface.
+- **Editor-only commands** — `AddOpenScriptCommand`, `BindTo`/`BindPropertyTo`, `EnumField`/`EnumFlagsField` `Initialize`, and `PropertyField` value-change subscriptions.
+- **USS custom-style helpers** — `ICustomStyle.TryGetByEnum` for parsing string USS properties as enums.
+
+> Full method-by-method reference: [VisualElementExtensions.md](VisualElementExtensions.md)
+
+### Full example
+
+```csharp
+using UnityEditor;
+using UnityEngine;
+using Aspid.FastTools.Editors; // GetScriptName
+using Aspid.FastTools.UIElements; // runtime VisualElement extensions
+using Aspid.FastTools.UIElements.Editors; // AddOpenScriptCommand
+using UnityEngine.UIElements;
+
+[CustomEditor(typeof(MyBehaviour))]
+public class MyBehaviourEditor : Editor
+{
+ public override VisualElement CreateInspectorGUI()
+ {
+ const string iconPath = "Editor/MyIcon";
+
+ var scriptName = target.GetScriptName();
+ var dark = new Color(0.15f, 0.15f, 0.15f);
+ var light = new Color(0.75f, 0.75f, 0.75f);
+
+ return new VisualElement()
+ .SetName("Header")
+ .SetBackgroundColor(dark)
+ .SetFlexDirection(FlexDirection.Row)
+ .SetPadding(top: 5, bottom: 5, left: 10, right: 10)
+ .SetBorderRadius(topLeft: 10, topRight: 10, bottomLeft: 10, bottomRight: 10)
+ .AddChild(new Image()
+ .SetName("Icon")
+ .AddOpenScriptCommand(target)
+ .SetImageFromResource(iconPath)
+ .SetSize(width: 40, height: 40))
+ .AddChild(new Label(scriptName)
+ .SetName("Title")
+ .SetFlexGrow(1)
+ .SetFontSize(16)
+ .SetMargin(left: 10)
+ .SetColor(light)
+ .SetAlignSelf(Align.Center)
+ .SetOverflow(Overflow.Hidden)
+ .SetWhiteSpace(WhiteSpace.NoWrap)
+ .SetTextOverflow(TextOverflow.Ellipsis)
+ .SetUnityFontStyleAndWeight(FontStyle.Bold));
+ }
+}
+```
+
+### Result
+
+
+
+---
+
+## Editor Helper Extensions
+
+Utility methods for getting display names of Unity objects in custom editors.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+```csharp
+public static string GetScriptName(this Object obj)
+```
+
+Returns the display name of a Unity object:
+- If the type has `[AddComponentMenu]`, returns `ObjectNames.GetInspectorTitle(obj)`
+- Otherwise returns `ObjectNames.NicifyVariableName(typeName)`
+
+```csharp
+public static string GetScriptNameWithIndex(this Component targetComponent)
+```
+
+Returns the display name with a count suffix when multiple components of the same type exist on the same GameObject. For example, if two `AudioSource` components are attached, the second returns `"Audio Source (2)"`.
+
+```csharp
+[CustomEditor(typeof(MyBehaviour))]
+public class MyBehaviourEditor : Editor
+{
+ public override VisualElement CreateInspectorGUI()
+ {
+ // "My Behaviour" — or "Custom Name" if [AddComponentMenu("Custom Name")] is present
+ var name = target.GetScriptName();
+
+ // "My Behaviour (2)" when a second component of the same type exists
+ var nameWithIndex = ((Component)target).GetScriptNameWithIndex();
+
+ return new Label(name);
+ }
+}
+```
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/README.md.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/README.md.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/README.md.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md
new file mode 100644
index 00000000..c22b34c2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md
@@ -0,0 +1,124 @@
+# SerializedProperty Extensions — full reference
+
+Chainable extension methods on `SerializedProperty` for synchronizing the owning `SerializedObject`, setting values, and reflecting on the underlying field.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+All extensions are generic over `T : SerializedProperty` and return the same property instance, so calls can be chained freely.
+
+## Update / Apply
+
+Thin wrappers around the matching `SerializedObject` methods on `property.serializedObject`.
+
+```csharp
+property
+ .Update()
+ .SetInt(42)
+ .ApplyModifiedProperties();
+```
+
+| Method | Description |
+|--------|-------------|
+| `Update()` | Calls `serializedObject.Update()` |
+| `UpdateIfRequiredOrScript()` | Calls `serializedObject.UpdateIfRequiredOrScript()` |
+| `ApplyModifiedProperties()` | Calls `serializedObject.ApplyModifiedProperties()` |
+
+## SetValue / SetXxx — typed setters
+
+For each supported type four variants exist:
+
+| Variant | Behavior |
+|---------|----------|
+| `SetValue(value)` | Generic dispatch — picks the right typed setter based on the value's runtime type, returns `property` |
+| `SetValueAndApply(value)` | `SetValue(value)` followed by `ApplyModifiedProperties()` |
+| `SetXxx(value)` | Typed setter (e.g. `SetInt`) that writes to the matching `SerializedProperty.xxxValue` field |
+| `SetXxxAndApply(value)` | `SetXxx(value)` followed by `ApplyModifiedProperties()` |
+
+### Supported types
+
+| Method family | Unity type | Notes |
+|---------------|------------|-------|
+| `SetInt` | `int` | |
+| `SetUint` | `uint` | |
+| `SetLong` | `long` | |
+| `SetUlong` | `ulong` | |
+| `SetFloat` | `float` | |
+| `SetDouble` | `double` | |
+| `SetBool` | `bool` | |
+| `SetString` | `string` | |
+| `SetColor` | `Color` | |
+| `SetGradient` | `Gradient` | |
+| `SetHash128` | `Hash128` | |
+| `SetRect` / `SetRectInt` | `Rect` / `RectInt` | |
+| `SetBounds` / `SetBoundsInt` | `Bounds` / `BoundsInt` | |
+| `SetVector2` / `SetVector2Int` | `Vector2` / `Vector2Int` | |
+| `SetVector3` / `SetVector3Int` | `Vector3` / `Vector3Int` | |
+| `SetVector4` | `Vector4` | |
+| `SetQuaternion` | `Quaternion` | |
+| `SetAnimationCurve` | `AnimationCurve` | |
+| `SetEntityId` | `Unity.Entities.EntityId` | Unity 6.2+. The apply-variant is named `SetEntityIdApply` *(method name preserves the source typo: missing `And`)* |
+
+### Enum setters
+
+Enum values do not flow through `SetValue` — use the explicit pair below depending on whether the field is a `[Flags]` enum:
+
+| Method | Description |
+|--------|-------------|
+| `SetEnumFlag(int)` / `SetEnumFlagAndApply(int)` | Writes to `enumValueFlag` |
+| `SetEnumIndex(int)` / `SetEnumIndexAndApply(int)` | Writes to `enumValueIndex` |
+
+### Example
+
+```csharp
+SerializedProperty property = GetProperty();
+
+// Equivalent forms
+property.SetValue(10).ApplyModifiedProperties();
+property.SetValueAndApply(10);
+property.SetInt(10).ApplyModifiedProperties();
+property.SetIntAndApply(10);
+
+// Chain multiple setters
+property
+ .SetVector3(Vector3.up)
+ .SetBool(true)
+ .ApplyModifiedProperties();
+```
+
+## Array operations
+
+| Method | Description |
+|--------|-------------|
+| `SetArraySize(int)` / `SetArraySizeAndApply(int)` | Sets `property.arraySize` |
+| `AddArraySize(int = 1)` / `AddArraySizeAndApply(int = 1)` | Increases `arraySize` by the given amount (default `1`) |
+| `RemoveArraySize(int = 1)` / `RemoveArraySizeAndApply(int = 1)` | Decreases `arraySize` by the given amount (default `1`) |
+
+## Reference setters
+
+| Method | Description | Notes |
+|--------|-------------|-------|
+| `SetManagedReference(object)` / `SetManagedReferenceAndApply(object)` | Writes to `managedReferenceValue` (target must be a `[SerializeReference]` field) | |
+| `SetObjectReference(Object)` / `SetObjectReferenceAndApply(Object)` | Writes to `objectReferenceValue` | |
+| `SetExposedReference(Object)` / `SetExposedReferenceAndApply(Object)` | Writes to `exposedReferenceValue` | |
+| `SetBoxed(object)` / `SetBoxedAndApply(object)` | Writes to `boxedValue` | Unity 6+ |
+
+## Reflection helpers
+
+For drawer / inspector code that needs to inspect the runtime type or instance behind a property:
+
+| Method | Returns | Description |
+|--------|---------|-------------|
+| `GetPropertyType()` | `Type` or `null` | Returns the `FieldType` / `PropertyType` of the C# member that backs the property. `null` if the member can't be resolved. |
+| `GetMemberInfo()` | `MemberInfo` or `null` | Locates the field/property on the owning class whose name matches `SerializedProperty.name`. Walks base classes via `TypeExtensions.GetMembersInfosIncludingBaseClasses`. |
+| `GetClassInstance()` | `object` | Walks `propertyPath` from the root `targetObject` and returns the runtime instance that directly contains this property. Supports nested objects, arrays, and `List` fields. |
+
+```csharp
+public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
+{
+ var declaringType = property.GetPropertyType();
+ var owner = property.GetClassInstance();
+ // …
+}
+```
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md.meta
new file mode 100644
index 00000000..78b1558d
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/SerializedPropertyExtensions.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d89b4f8675a09411280afbba6c1aaf33
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md
new file mode 100644
index 00000000..c6a44b57
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md
@@ -0,0 +1,600 @@
+# VisualElement Extensions — full reference
+
+Fluent extension methods for building UIToolkit trees in code. All methods return `T` (the element itself) for chaining.
+
+```csharp
+using Aspid.FastTools.UIElements; // runtime extensions
+using Aspid.FastTools.UIElements.Editors; // editor-only extensions (e.g. AddOpenScriptCommand)
+```
+
+## Core element operations
+
+```csharp
+element
+ .SetName("MyElement")
+ .SetVisible(true)
+ .SetTooltip("Tooltip text")
+ .AddChild(new Label("Hello"))
+ .AddChildren(child1, child2, child3);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetName(string)` | Sets `element.name` |
+| `SetVisible(bool)` | Sets `element.visible` |
+| `SetTooltip(string)` | Sets `element.tooltip` |
+| `SetUserData(object)` | Sets `element.userData` |
+| `SetEnabledSelf(bool)` | Sets `element.enabledSelf` |
+| `SetPickingMode(PickingMode)` | Sets `element.pickingMode` |
+| `SetUsageHints(UsageHints)` | Sets `element.usageHints` |
+| `SetViewDataKey(string)` | Sets `element.viewDataKey` |
+| `SetLanguageDirection(LanguageDirection)` | Sets `element.languageDirection` |
+| `SetDisablePlayModeTint(bool)` | Sets `element.disablePlayModeTint` |
+| `SetDataSource(object)` | Sets `element.dataSource` |
+| `SetDataSourceType(Type)` | Sets `element.dataSourceType` |
+| `SetDataSourcePath(PropertyPath)` | Sets `element.dataSourcePath` |
+| `AddChild(VisualElement)` | Appends a child, returns the parent |
+| `AddChildren(params VisualElement[])` | Appends multiple children |
+| `AddChildren(IEnumerable)` | Appends from a sequence |
+| `AddChildren(List)` | Appends from a list |
+| `AddChildren(Span)` | Appends from a span |
+| `AddChildren(ReadOnlySpan)` | Appends from a read-only span |
+| `InsertChild(int, VisualElement)` | Inserts a child at the specified index |
+| `InsertChildren(int, params VisualElement[])` | Inserts multiple children starting at an index |
+| `InsertChildren(int, IEnumerable)` | Inserts from a sequence |
+| `InsertChildren(int, List)` | Inserts from a list |
+| `InsertChildren(int, Span)` | Inserts from a span |
+| `InsertChildren(int, ReadOnlySpan)` | Inserts from a read-only span |
+
+> `RegisterCallbackOnce` and `RegisterCallbackOnce` are available on all Unity versions (polyfill included for versions prior to 2023.1).
+
+## Focusable
+
+| Method | Description |
+|--------|-------------|
+| `SetFocus()` | Attempts to give focus to the element |
+| `SetBlur()` | Tells the element to release focus |
+| `IsFocus()` | Returns whether the element currently has keyboard focus |
+| `SetTabIndex(int)` | Sets `element.tabIndex` |
+| `SetFocusable(bool)` | Sets `element.focusable` |
+| `SetDelegatesFocus(bool)` | Sets `element.delegatesFocus` |
+
+## USS & class operations
+
+| Method | Description |
+|--------|-------------|
+| `AddClass(string)` | Adds a USS class |
+| `RemoveClass(string)` | Removes a USS class |
+| `ClearClasses()` | Removes all USS classes |
+| `ToggleInClass(string)` | Toggles a USS class on/off |
+| `EnableInClass(string, bool)` | Adds or removes a USS class based on a condition |
+| `AddStyleSheets(StyleSheet)` | Adds a `StyleSheet` |
+| `RemoveStyleSheets(StyleSheet)` | Removes a `StyleSheet` |
+| `AddStyleSheetsFromResource(string)` | Adds a stylesheet loaded via `Resources.Load` |
+| `RemoveStyleSheetsFromResource(string)` | Removes a stylesheet loaded via `Resources.Load` |
+
+## Style extensions — by category
+
+All style methods are also available on `IStyle` directly (same method names, operate on the style object).
+
+### Layout
+
+| Method | Style property |
+|--------|---------------|
+| `SetFlexBasis(StyleLength)` | `flexBasis` |
+| `SetFlexGrow(StyleFloat)` | `flexGrow` |
+| `SetFlexShrink(StyleFloat)` | `flexShrink` |
+| `SetFlexWrap(StyleEnum)` | `flexWrap` |
+| `SetFlexDirection(FlexDirection)` | `flexDirection` |
+| `SetAlignSelf(StyleEnum)` | `alignSelf` |
+| `SetAlignItems(StyleEnum)` | `alignItems` |
+| `SetAlignContent(StyleEnum)` | `alignContent` |
+| `SetJustifyContent(StyleEnum)` | `justifyContent` |
+| `SetPosition(StyleEnum)` | `position` |
+
+### Size
+
+| Method | Description |
+|--------|-------------|
+| `SetSize(StyleLength)` | Sets both width and height |
+| `SetSize(width?, height?)` | Sets width and/or height independently |
+| `SetMinSize(StyleLength)` | Sets both minWidth and minHeight |
+| `SetMinSize(width?, height?)` | |
+| `SetMaxSize(StyleLength)` | Sets both maxWidth and maxHeight |
+| `SetMaxSize(width?, height?)` | |
+| `SetWidth(StyleLength)` | `width` |
+| `SetMinWidth(StyleLength)` | `minWidth` |
+| `SetMaxWidth(StyleLength)` | `maxWidth` |
+| `SetHeight(StyleLength)` | `height` |
+| `SetMinHeight(StyleLength)` | `minHeight` |
+| `SetMaxHeight(StyleLength)` | `maxHeight` |
+
+### Spacing
+
+All spacing methods have a uniform-value overload, a per-side overload (`top`, `right`, `bottom`, `left`), single-side setters, and X/Y-axis pair setters.
+
+| Method | Style properties |
+|--------|------------------|
+| `SetMargin(…)` / `SetPadding(…)` / `SetDistance(…)` | `Top/Right/Bottom/Left` (uniform or per-side) |
+| `SetMarginX/Y` · `SetPaddingX/Y` · `SetDistanceX/Y` | Sets the horizontal (X = `Left`+`Right`) or vertical (Y = `Top`+`Bottom`) pair |
+| `SetMarginTop/Right/Bottom/Left` | Single-side margin |
+| `SetPaddingTop/Right/Bottom/Left` | Single-side padding |
+| `SetDistanceTop/Right/Bottom/Left` *(via `SetTop` / `SetRight` / `SetBottom` / `SetLeft`)* | Single-side absolute offset (`top` / `right` / `bottom` / `left` style properties) |
+
+> `SetDistance` is the wrapper for the four `top`/`right`/`bottom`/`left` style properties used by absolute positioning. `SetTop`, `SetRight`, `SetBottom`, `SetLeft` are direct single-property aliases.
+
+### Font
+
+| Method | Style property |
+|--------|---------------|
+| `SetUnityFont(StyleFont)` | `unityFont` |
+| `SetFontSize(StyleLength)` | `fontSize` |
+| `SetUnityFontDefinition(StyleFontDefinition)` | `unityFontDefinition` |
+| `SetUnityFontStyleAndWeight(StyleEnum)` | `unityFontStyleAndWeight` |
+
+### Font style presets
+
+Convenience methods for toggling bold / italic without overwriting the other flag:
+
+| Method | Description |
+|--------|-------------|
+| `SetNormalUnityFontStyleAndWeight()` | Resets to `FontStyle.Normal` |
+| `AddBoldUnityFontStyleAndWeight()` | Adds bold, preserving italic |
+| `RemoveBoldUnityFontStyleAndWeight()` | Removes bold, preserving italic |
+| `AddItalicUnityFontStyleAndWeight()` | Adds italic, preserving bold |
+| `RemoveItalicUnityFontStyleAndWeight()` | Removes italic, preserving bold |
+
+### Text
+
+| Method | Style property | Notes |
+|--------|---------------|-------|
+| `SetWorldSpacing(StyleLength)` | `wordSpacing` | |
+| `SetLetterSpacing(StyleLength)` | `letterSpacing` | |
+| `SetUnityTextAlign(TextAnchor)` | `unityTextAlign` | |
+| `SetTextShadow(StyleTextShadow)` | `textShadow` | |
+| `SetUnityTextOutlineColor(StyleColor)` | `unityTextOutlineColor` | |
+| `SetUnityTextOutlineWidth(StyleFloat)` | `unityTextOutlineWidth` | |
+| `SetUnityParagraphSpacing(StyleLength)` | `unityParagraphSpacing` | |
+| `SetTextOverflow(StyleEnum)` | `textOverflow` | |
+| `SetUnityTextOverflowPosition(TextOverflowPosition)` | `unityTextOverflowPosition` | |
+| `SetUnityTextGenerator(TextGeneratorType)` | `unityTextGenerator` | Unity 6+ |
+| `SetUnityEditorTextRenderingMode(EditorTextRenderingMode)` | `unityEditorTextRenderingMode` | Unity 6+ |
+| `SetUnityTextAutoSize(StyleTextAutoSize)` | `unityTextAutoSize` | Unity 6.2+ |
+| `SetWhiteSpace(StyleEnum)` | `whiteSpace` | |
+
+### Color & Opacity
+
+| Method | Style property |
+|--------|---------------|
+| `SetColor(StyleColor)` | `color` |
+| `SetColor(string)` | `color` parsed from an HTML string (`"#RRGGBB"` or a named color) |
+| `SetOpacity(StyleFloat)` | `opacity` |
+
+### Border
+
+| Method | Description |
+|--------|-------------|
+| `SetBorderColor(StyleColor)` | All sides |
+| `SetBorderColor(top?, right?, bottom?, left?)` | Per side |
+| `SetBorderColorX(StyleColor)` · `SetBorderColorY(StyleColor)` | Horizontal (left + right) or vertical (top + bottom) pair |
+| `SetBorderColorTop/Right/Bottom/Left(StyleColor)` | Single side |
+| `SetBorderRadius(StyleLength)` | All corners |
+| `SetBorderRadius(topLeft?, topRight?, bottomLeft?, bottomRight?)` | Per corner |
+| `SetBorderRadiusTop(StyleLength)` · `SetBorderRadiusBottom(StyleLength)` | Top or bottom corner pair |
+| `SetBorderRadiusTopLeft/TopRight/BottomLeft/BottomRight(StyleLength)` | Single corner |
+| `SetBorderWidth(StyleFloat)` | All sides |
+| `SetBorderWidth(top?, right?, bottom?, left?)` | Per side |
+| `SetBorderWidthX(StyleFloat)` · `SetBorderWidthY(StyleFloat)` | Horizontal or vertical pair |
+| `SetBorderWidthTop/Right/Bottom/Left(StyleFloat)` | Single side |
+
+### Background
+
+| Method | Style property |
+|--------|---------------|
+| `SetBackgroundColor(StyleColor)` | `backgroundColor` |
+| `SetBackgroundColor(string)` | `backgroundColor` parsed from an HTML string (`"#RRGGBB"` or a named color) |
+| `SetBackgroundImage(StyleBackground)` | `backgroundImage` |
+| `SetBackgroundImageFromResource(string)` | Loads a `Texture2D` via `Resources.Load` and assigns it to `backgroundImage` |
+| `SetBackgroundSize(StyleBackgroundSize)` | `backgroundSize` |
+| `SetBackgroundRepeat(StyleBackgroundRepeat)` | `backgroundRepeat` |
+| `SetBackgroundPosition(StyleBackgroundPosition)` | Both X and Y |
+| `SetBackgroundPosition(x?, y?)` | Independently |
+| `SetBackgroundPositionX(StyleBackgroundPosition)` | `backgroundPositionX` |
+| `SetBackgroundPositionY(StyleBackgroundPosition)` | `backgroundPositionY` |
+| `SetUnityBackgroundImageTintColor(StyleColor)` | `unityBackgroundImageTintColor` |
+
+### Transform
+
+| Method | Style property |
+|--------|---------------|
+| `SetScale(StyleScale)` | `scale` |
+| `SetRotate(StyleRotate)` | `rotate` |
+| `SetTranslate(StyleTranslate)` | `translate` |
+| `SetTransformOrigin(StyleTransformOrigin)` | `transformOrigin` |
+
+### Aspect, Filter & Material
+
+Available on Unity 6000.3+.
+
+| Method | Style property |
+|--------|---------------|
+| `SetAspectRation(StyleRatio)` | `aspectRatio` *(method name preserves the source typo)* |
+| `SetFilter(StyleList)` | `filter` |
+| `SetUnityMaterial(StyleMaterialDefinition)` | `unityMaterial` |
+
+### Transition
+
+| Method | Style property |
+|--------|---------------|
+| `SetTransitionDelay(StyleList)` | `transitionDelay` |
+| `SetTransitionDuration(StyleList)` | `transitionDuration` |
+| `SetTransitionProperty(StyleList)` | `transitionProperty` |
+| `SetTransitionTimingFunction(StyleList)` | `transitionTimingFunction` |
+
+### Overflow & Visibility
+
+| Method | Style property |
+|--------|---------------|
+| `SetOverflow(StyleEnum)` | `overflow` |
+| `SetUnityOverflowClipBox(StyleEnum)` | `unityOverflowClipBox` |
+| `SetVisibility(StyleEnum)` | `visibility` |
+| `SetDisplay(DisplayStyle)` | `display` |
+
+### Unity Slice
+
+| Method | Description |
+|--------|-------------|
+| `SetUnitySlice(StyleInt)` | All sides |
+| `SetUnitySlice(top?, right?, bottom?, left?)` | Per side |
+| `SetUnitySliceX(StyleInt)` · `SetUnitySliceY(StyleInt)` | Horizontal (left + right) or vertical (top + bottom) pair |
+| `SetUnitySliceTop/Right/Bottom/Left(StyleInt)` | Single side |
+| `SetUnitySliceScale(StyleFloat)` | `unitySliceScale` |
+| `SetUnitySliceType(StyleEnum)` | Unity 6+ |
+
+### Cursor
+
+| Method | Style property |
+|--------|---------------|
+| `SetCursor(StyleCursor)` | `cursor` |
+
+## Specialized element extensions
+
+### TextElement
+
+```csharp
+label
+ .SetText("Hello World")
+ .SetEnableRichText(true)
+ .SetParseEscapeSequences(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetText(string)` | Sets the displayed text |
+| `SetEnableRichText(bool)` | Enables rich-text tag parsing |
+| `SetEmojiFallbackSupport(bool)` | Enables emoji fallback rendering |
+| `SetParseEscapeSequences(bool)` | Whether escape sequences (e.g. `\n`) are parsed |
+| `SetDisplayTooltipWhenElided(bool)` | Shows the elided text in a tooltip on hover |
+
+### ITextEdition (TextField, IntegerField, …)
+
+```csharp
+textField
+ .SetPlaceholder("Search…")
+ .SetMaxLength(64)
+ .SetIsDelayed(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetMaxLength(int)` | Maximum number of characters |
+| `SetMaskChar(char)` | Character used to mask password input |
+| `SetIsDelayed(bool)` | Defers value change until focus loss / Enter |
+| `SetIsReadOnly(bool)` | Disables editing |
+| `SetIsPassword(bool)` | Toggles password mode (uses mask char) |
+| `SetPlaceholder(string)` | Placeholder text shown when empty |
+| `SetAutoCorrection(bool)` | Enables auto-correction (mobile) |
+| `SetHideMobileInput(bool)` | Hides the mobile soft input |
+| `SetHideSoftKeyboard(bool)` | Hides the on-screen soft keyboard |
+| `SetHidePlaceholderOnFocus(bool)` | Removes the placeholder on focus |
+| `SetKeyboardType(TouchScreenKeyboardType)` | Sets the touch-screen keyboard type |
+
+### ITextSelection
+
+```csharp
+textField
+ .SetIsSelectable(true)
+ .SetSelectAllOnFocus(true)
+ .AddOnCursorIndexChange(() => Debug.Log(textField.cursorIndex));
+```
+
+| Method | Description |
+|--------|-------------|
+| `AddOnCursorIndexChange(Action)` / `RemoveOnCursorIndexChange(Action)` | Cursor-index change subscription |
+| `AddOnSelectIndexChange(Action)` / `RemoveOnSelectIndexChange(Action)` | Selection-index change subscription |
+| `SetCursorIndex(int)` | Sets the current cursor index |
+| `SetSelectIndex(int)` | Sets the current selection anchor |
+| `SetIsSelectable(bool)` | Whether text can be selected |
+| `SetSelectAllOnFocus(bool)` | Selects all text on focus |
+| `SetSelectAllOnMouseUp(bool)` | Selects all text on mouse release |
+| `SetDoubleClickSelectsWord(bool)` | Double-click selects the word under cursor |
+| `SetTripleClickSelectsLine(bool)` | Triple-click selects the line under cursor |
+
+### BaseField\
+
+```csharp
+field.SetLabel("My Field");
+field.SetValue(42);
+```
+
+### BaseBoolField (Toggle)
+
+```csharp
+toggle
+ .SetLabel("Enabled")
+ .SetText("Show advanced settings")
+ .SetToggleOnLabelClick(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetText(string)` | Sets the label next to the toggle box |
+| `SetLabel(string)` | Sets the field-level label |
+| `SetToggleOnLabelClick(bool)` | Whether clicking the label toggles the value |
+
+### INotifyValueChanged\
+
+```csharp
+field.SetValue(42, notify: false); // sets value without raising ChangeEvent
+field.AddValueChanged(evt => Debug.Log(evt.newValue));
+field.RemoveValueChanged(myCallback);
+```
+
+Typed overloads are provided for `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID`, `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, `object`, plus a generic `SetValue` fallback.
+
+> When the `com.unity.mathematics` package is installed, the `ASPID_FASTTOOLS_UNITY_MATHEMATICS_INTEGRATION` define is set automatically and adds `SetValue` / `AddValueChanged` / `RemoveValueChanged` overloads for `int2/3/4` (and `intMxN`), `float2/3/4` (and `floatMxN`), `bool2/3/4` (and `boolMxN`), and `quaternion`.
+
+### IMixedValueSupport
+
+```csharp
+field.SetShowMixedValue(true); // shows the mixed-value indicator
+```
+
+### Button
+
+```csharp
+button
+ .AddClicked(() => Debug.Log("Clicked"))
+ .SetClickable(new Clickable(() => { }))
+ .SetIconImage(myBackground);
+```
+
+| Method | Description |
+|--------|-------------|
+| `AddClicked(Action)` | Subscribes to `Button.clicked` |
+| `RemoveClicked(Action)` | Unsubscribes from `Button.clicked` |
+| `SetClickable(Clickable)` | Sets `Button.clickable` |
+| `SetIconImage(Background)` | Sets `Button.iconImage` |
+
+### Slider / BaseSlider\
+
+```csharp
+slider
+ .SetLowValue(0f)
+ .SetHighValue(100f)
+ .SetShowInputField(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetLowValue(TValue)` | Sets the minimum slider value |
+| `SetHighValue(TValue)` | Sets the maximum slider value |
+| `SetFill(bool)` | Whether the track is filled up to the current value |
+| `SetInverted(bool)` | Reverses the slider direction |
+| `SetPageSize(float)` | Controls how much the value changes per page step |
+| `SetShowInputField(bool)` | Shows a numeric input field alongside the slider |
+| `SetDirection(SliderDirection)` | Sets the slider orientation |
+
+### ProgressBar
+
+```csharp
+progressBar.SetTitle("Loading...").SetLowValue(0f).SetHighValue(100f);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetTitle(string)` | Sets the title displayed in the center |
+| `SetLowValue(float)` | Sets the minimum value |
+| `SetHighValue(float)` | Sets the maximum value |
+
+### HelpBox
+
+```csharp
+helpBox
+ .SetText("Something went wrong")
+ .SetMessageType(HelpBoxMessageType.Warning);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetText(string)` | Sets the help-box message text |
+| `SetMessageType(HelpBoxMessageType)` | Sets the icon / severity (`None` / `Info` / `Warning` / `Error`) |
+
+### Foldout
+
+```csharp
+foldout
+ .SetText("Section Title")
+ .SetToggleOnLabelClick(true)
+ .SetValue(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetText(string)` | Sets the foldout title |
+| `SetToggleOnLabelClick(bool)` | Whether clicking the title toggles expansion |
+
+### Image
+
+```csharp
+image
+ .SetImage(myTexture)
+ .SetTintColor(Color.white)
+ .SetScaleMode(ScaleMode.ScaleToFit);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetImage(Texture)` | Sets `Image.image` |
+| `SetImageFromResource(string)` | Loads a texture via `Resources.Load` |
+| `SetSprite(Sprite)` | Sets `Image.sprite` |
+| `SetSpriteFromResource(string)` | Loads a sprite via `Resources.Load` |
+| `SetVectorImage(VectorImage)` | Sets `Image.vectorImage` |
+| `SetVectorImageFromResource(string)` | Loads a vector image via `Resources.Load` |
+| `SetUv(Rect)` | Sets the UV rect |
+| `SetSourceRect(Rect)` | Sets the source rect |
+| `SetTintColor(Color)` | Sets the image tint |
+| `SetScaleMode(ScaleMode)` | Sets the scale mode |
+
+### IMGUIContainer
+
+```csharp
+container
+ .SetOnGUIHandler(() => GUILayout.Label("IMGUI"))
+ .SetCullingEnabled(true);
+```
+
+| Method | Description |
+|--------|-------------|
+| `SetOnGUIHandler(Action)` | Replaces the `onGUIHandler` callback |
+| `AddOnGUIHandler(Action)` | Subscribes to `onGUIHandler` |
+| `RemoveOnGUIHandler(Action)` | Unsubscribes from `onGUIHandler` |
+| `SetCullingEnabled(bool)` | Skips `onGUIHandler` when the element is offscreen |
+| `SetContextType(ContextType)` | Sets the IMGUI context type |
+
+### Collection views (ListView, TreeView, MultiColumn variants)
+
+Common methods are spread across multiple targeted extensions:
+
+- `BaseVerticalCollectionViewExtensions` — applies to **all** collection views (ListView, TreeView, MultiColumn variants).
+- `BaseListViewExtensions` — applies to ListView and MultiColumnListView.
+- `BaseTreeViewExtensions` — applies to TreeView and MultiColumnTreeView.
+- `ListViewExtensions` / `TreeViewExtensions` — `MakeItem`/`BindItem`/`UnbindItem`/`DestroyItem` factories per view.
+- `MultiColumnListViewExtensions` / `MultiColumnTreeViewExtensions` — multi-column-specific helpers.
+
+```csharp
+listView
+ .SetItemsSource(items)
+ .SetMakeItem(() => new Label())
+ .SetBindItem((el, i) => ((Label)el).SetText(items[i]))
+ .SetSelectionType(SelectionType.Single)
+ .AddSelectionChanged(selected => Debug.Log(selected));
+```
+
+#### Source, layout and behavior — `BaseVerticalCollectionView`
+
+| Method | Description | Notes |
+|--------|-------------|-------|
+| `SetItemsSource(IList)` | Underlying data source | |
+| `SetReorderable(bool)` | Enables drag-to-reorder | |
+| `SetSelectedIndex(int)` | Selects a specific index | |
+| `SetSelectionType(SelectionType)` | None / Single / Multiple | |
+| `SetFixedItemHeight(float)` | Fixed item height (for `FixedHeight` virtualization) | |
+| `SetVirtualizationMethod(CollectionVirtualizationMethod)` | `FixedHeight` or `DynamicHeight` | |
+| `SetHorizontalScrollingEnabled(bool)` | Enables horizontal scrolling | |
+| `SetShowAlternatingRowBackgrounds(AlternatingRowBackground)` | Zebra striping mode | |
+| `SetMakeFooter(Func)` · `AddMakeFooter` · `RemoveMakeFooter` | Footer factory | Unity 6+ |
+| `SetMakeHeader(Func)` · `AddMakeHeader` · `RemoveMakeHeader` | Header factory | Unity 6+ |
+| `SetMakeNoneElement(Func)` · `AddMakeNoneElement` · `RemoveMakeNoneElement` | Empty-state factory | Unity 6+ |
+
+#### Events — `BaseVerticalCollectionView`
+
+| Method | Description |
+|--------|-------------|
+| `AddItemsChosen(Action>)` / `RemoveItemsChosen` | Items confirmed (e.g. double-click / Enter) |
+| `AddSelectionChanged(Action>)` / `RemoveSelectionChanged` | Selection changed (objects) |
+| `AddSelectedIndicesChanged(Action>)` / `RemoveSelectedIndicesChanged` | Selection changed (indices) |
+| `AddItemIndexChanged(Action)` / `RemoveItemIndexChanged` | Item moved (drag-reorder) |
+| `AddItemsSourceChanged(Action)` / `RemoveItemsSourceChanged` | `itemsSource` reference changed |
+| `AddCanStartDrag(Func)` / `RemoveCanStartDrag` | Custom drag-start gating |
+| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Drag-and-drop preparation |
+| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Drag-and-drop visual mode |
+| `AddHandleDrop(Func)` / `RemoveHandleDrop` | Drop handling |
+
+#### `BaseListView`-specific
+
+| Method | Description |
+|--------|-------------|
+| `SetAllowAdd(bool)` · `SetAllowRemove(bool)` | Toggles built-in add/remove buttons |
+| `SetHeaderTitle(string)` | Title shown when foldout header is on |
+| `SetShowFoldoutHeader(bool)` | Wraps the list in a `Foldout` |
+| `SetShowAddRemoveFooter(bool)` | Toggles the add/remove footer |
+| `SetShowBoundCollectionSize(bool)` | Shows the collection-size field |
+| `SetReorderMode(ListViewReorderMode)` | `Simple` or `Animated` |
+| `SetBindingSourceSelectionMode(BindingSourceSelectionMode)` | Auto-assign / manual |
+| `SetOnAdd(Action)` · `AddOnAdd` · `RemoveOnAdd` | Custom add-button callback |
+| `SetOnRemove(Action)` · `AddOnRemove` · `RemoveOnRemove` | Custom remove-button callback |
+| `SetOverridingAddButtonBehavior(Action)` · `AddOverridingAddButtonBehavior` · `RemoveOverridingAddButtonBehavior` | Replace default add-button click |
+| `AddItemsAdded(Action>)` / `RemoveItemsAdded` | Items added by index |
+| `AddItemsRemoved(Action>)` / `RemoveItemsRemoved` | Items removed by index |
+
+#### `BaseTreeView`-specific
+
+| Method | Description |
+|--------|-------------|
+| `SetAutoExpand(bool)` | Auto-expand new nodes |
+| `AddItemExpandedChanged(Action)` / `RemoveItemExpandedChanged` | Subscription to expansion changes |
+
+#### `ListView` / `TreeView` item factories
+
+These methods are duplicated across `ListViewExtensions` and `TreeViewExtensions` (each operating on its own view type).
+
+| Method | Description |
+|--------|-------------|
+| `SetMakeItem(Func)` · `AddMakeItem` · `RemoveMakeItem` | Item factory |
+| `SetBindItem(Action)` · `AddBindItem` · `RemoveBindItem` | Item binding |
+| `SetUnbindItem(Action)` · `AddUnbindItem` · `RemoveUnbindItem` | Item unbinding |
+| `SetDestroyItem(Action)` · `AddDestroyItem` · `RemoveDestroyItem` | Item teardown |
+| `SetItemTemplate(VisualTreeAsset)` | UXML template used to build items |
+
+#### `MultiColumnListView` / `MultiColumnTreeView`
+
+| Method | Description |
+|--------|-------------|
+| `SetSortingMode(ColumnSortingMode)` | Built-in sorting mode for the column header |
+
+## Editor commands (editor-only)
+
+```csharp
+using Aspid.FastTools.UIElements.Editors;
+
+image.AddOpenScriptCommand(target);
+// Double-clicking the element opens the script for 'target' in the IDE
+```
+
+| Method | Target | Description |
+|--------|--------|-------------|
+| `AddOpenScriptCommand(Object)` | `VisualElement` | Registers a double-click handler that opens the source script for the given `MonoBehaviour` / `ScriptableObject` in the IDE. |
+| `BindTo(SerializedObject)` | `VisualElement` | Calls `BindingExtensions.Bind` on the element. |
+| `BindTo(SerializedObject, string propertyPath)` | `IBindable` | Sets `bindingPath` and binds to the given `SerializedObject`. |
+| `BindPropertyTo(SerializedProperty)` | `IBindable` | Calls `BindingExtensions.BindProperty` with the supplied property. |
+| `Initialize(Enum defaultValue, bool includeObsoleteValues = false)` | `EnumField` / `EnumFlagsField` | Initializes the field to the supplied default enum value. |
+| `AddValueChanged(EventCallback)` / `RemoveValueChanged(...)` | `PropertyField` | Subscribes / unsubscribes to property change notifications. |
+
+## USS custom-style helpers (`ICustomStyle`)
+
+```csharp
+using Aspid.FastTools.UIElements;
+
+private static readonly CustomStyleProperty ThemeProperty = new("--aspid-fasttools-prop-theme");
+
+void OnCustomStyleResolved(CustomStyleResolvedEvent evt)
+{
+ if (evt.customStyle.TryGetByEnum(ThemeProperty, out ThemeStyle.Type theme))
+ ApplyTheme(theme);
+}
+```
+
+| Method | Description |
+|--------|-------------|
+| `ICustomStyle.TryGetByEnum(CustomStyleProperty, out T)` | Resolves a string-typed USS custom property and parses it case-insensitively as the enum `T`. Used by every `*Style` struct that exposes a USS-driven enum (`ThemeStyle`, `StatusStyle`, `AspidLabelSizeStyle`, etc.). |
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md.meta
new file mode 100644
index 00000000..9a0b54f9
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/EN/VisualElementExtensions.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a732922edf5904ae49ce09e54bf91bf6
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png
new file mode 100644
index 00000000..a332f4e2
Binary files /dev/null and b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png differ
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png.meta
new file mode 100644
index 00000000..683f8d22
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.SerializableType.png.meta
@@ -0,0 +1,143 @@
+fileFormatVersion: 2
+guid: b0d82cbe15d4842dd9d7c31cb4040e88
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: iOS
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png
new file mode 100644
index 00000000..5c979403
Binary files /dev/null and b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png differ
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png.meta
new file mode 100644
index 00000000..7553ff5d
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.TypeSelectorWindow.png.meta
@@ -0,0 +1,143 @@
+fileFormatVersion: 2
+guid: 648f40dc17f7a46a99e35e8dcf5318e7
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: iOS
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.VisualElement.png b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.VisualElement.png
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.VisualElement.png
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.VisualElement.png
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.VisualElement.png.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.VisualElement.png.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.VisualElement.png.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/Aspid.FastTools.VisualElement.png.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/aspid_fasttools_profiler_markers.png b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/aspid_fasttools_profiler_markers.png
new file mode 100644
index 00000000..3fad4c0a
Binary files /dev/null and b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/aspid_fasttools_profiler_markers.png differ
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.ProfilerMarkers.png.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/aspid_fasttools_profiler_markers.png.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Documentation/Images/Aspid.UnityFastTools.ProfilerMarkers.png.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/Images/aspid_fasttools_profiler_markers.png.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU.meta
new file mode 100644
index 00000000..52eac5fd
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a64f88afda4440d4a0e4c182bcf7ac1e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md
new file mode 100644
index 00000000..2835af33
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md
@@ -0,0 +1,535 @@
+# Aspid.FastTools
+
+**Aspid.FastTools** — набор инструментов, предназначенных для минимизации рутинного написания кода в Unity.
+
+## Исходный код
+
+[[Aspid.FastTools](https://github.com/VPDPersonal/Aspid.FastTools)]
+
+
+---
+
+## Интеграция
+
+Установите Aspid.FastTools одним из следующих способов:
+
+- **Скачать .unitypackage** — Перейдите на [страницу релизов GitHub](https://github.com/VPDPersonal/Aspid.FastTools/releases) и скачайте последнюю версию `Aspid.FastTools.X.X.X.unitypackage`. Импортируйте его в проект.
+- **Через UPM** (Unity Package Manager) подключите следующий пакет:
+ - `https://github.com/VPDPersonal/Aspid.FastTools.git?path=Aspid.FastTools/Assets/Plugins/Aspid/FastTools`
+
+---
+
+## Поддержать проект
+
+Этот проект разрабатывается на добровольной основе. Если он оказался для вас полезным, вы можете поддержать его развитие финансово. Это поможет уделять больше времени улучшению и сопровождению **Aspid.FastTools**.
+
+Поддержать проект можно через следующие платформы:
+* \[[Unity Asset Store](https://assetstore.unity.com/packages/slug/365584)\]
+
+---
+
+## Пространства имён
+
+| Пространство имён | Описание |
+|-------------------|----------|
+| `Aspid.FastTools.Types` | `SerializableType`, `SerializableType`, `ComponentTypeSelector`, `TypeSelectorAttribute` |
+| `Aspid.FastTools.Enums` | `EnumValues` |
+| `Aspid.FastTools.Ids` | `IId`, `UniqueIdAttribute`, `IdRegistry` |
+| `Aspid.FastTools.UIElements` | Runtime fluent-расширения `VisualElement` |
+| `Aspid.FastTools.Editors` | Редакторские утилиты — расширения `SerializedProperty`, IMGUI-области, `GetScriptName` |
+| `Aspid.FastTools.Types.Editors` · `.Enums.Editors` · `.Ids.Editors` · `.UIElements.Editors` | Редакторский код по фичам (property drawers, инспектор реестров, editor-only расширения `VisualElement`) |
+
+---
+
+## ProfilerMarker
+
+Предоставляет регистрацию `ProfilerMarker` через source generation. Генератор создаёт статический маркер для каждого места вызова, идентифицируемый по вызывающему методу и номеру строки.
+
+```csharp
+using UnityEngine;
+
+public class MyBehaviour : MonoBehaviour
+{
+ private void Update()
+ {
+ DoSomething1();
+ DoSomething2();
+ }
+
+ private void DoSomething1()
+ {
+ using var _ = this.Marker();
+ // Некоторый код
+ }
+
+ private void DoSomething2()
+ {
+ using (this.Marker())
+ {
+ // Некоторый код
+ using var _ = this.Marker().WithName("Calculate");
+ // Некоторый код
+ }
+ }
+}
+```
+
+### Сгенерированный код
+
+```csharp
+using Unity.Profiling;
+using System.Runtime.CompilerServices;
+
+internal static class __MyBehaviourProfilerMarkerExtensions
+{
+ private static readonly ProfilerMarker DoSomething1_Marker_Line_13 = new("MyBehaviour.DoSomething1 (13)");
+ private static readonly ProfilerMarker DoSomething2_Marker_Line_19 = new("MyBehaviour.DoSomething2 (19)");
+ private static readonly ProfilerMarker DoSomething2_Marker_Line_22 = new("MyBehaviour.Calculate (22)");
+
+ public static ProfilerMarker.AutoScope Marker(this MyBehaviour _, [CallerLineNumberAttribute] int line = -1)
+ {
+#if ENABLE_PROFILER
+ if (line is 13) return DoSomething1_Marker_Line_13.Auto();
+ if (line is 19) return DoSomething2_Marker_Line_19.Auto();
+ if (line is 22) return DoSomething2_Marker_Line_22.Auto();
+#endif
+ return default;
+ }
+}
+```
+
+### Результат
+
+
+
+---
+
+## Система сериализуемых типов
+
+Позволяет сериализовать ссылку на `System.Type` в Unity Inspector. Выбранный тип хранится как assembly-qualified name и разрешается лениво при первом обращении.
+
+### SerializableType
+
+Доступны два варианта:
+
+- **`SerializableType`** — хранит любой тип (базовый тип — `object`)
+- **`SerializableType`** — хранит тип, ограниченный `T` или его подклассами
+
+Оба поддерживают неявное преобразование в `System.Type`.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public class MyBehaviour : MonoBehaviour
+{
+ [SerializeField] private SerializableType _anyType;
+ [SerializeField] private SerializableType _behaviourType;
+
+ private void Start()
+ {
+ Type type1 = _anyType; // неявный оператор
+ Type type2 = _behaviourType.Type; // явное свойство
+
+ var instance = (MonoBehaviour)gameObject.AddComponent(type2);
+ }
+}
+```
+
+### ComponentTypeSelector
+
+Сериализуемая структура, добавляющая в Inspector выпадающий список для смены типа объекта. Добавьте её как поле в базовый класс — при выборе подтипа редактор перезаписывает `m_Script` на `SerializedObject`, фактически превращая компонент или ScriptableObject в выбранный подтип.
+
+Список автоматически ограничивается подтипами класса, в котором объявлено поле. Дополнительная настройка не требуется.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public abstract class BaseEnemy : MonoBehaviour
+{
+ [SerializeField] private ComponentTypeSelector _typeSelector;
+}
+
+public class FastEnemy : BaseEnemy { }
+public class TankEnemy : BaseEnemy { }
+```
+
+---
+
+### TypeSelectorAttribute
+
+Атрибут `PropertyAttribute`, доступный только в редакторе, ограничивающий всплывающее окно выбора типа конкретными базовыми типами. Применяется к полям `string`, хранящим assembly-qualified имена типов.
+
+```csharp
+[Conditional("UNITY_EDITOR")]
+public sealed class TypeSelectorAttribute : PropertyAttribute
+{
+ public TypeSelectorAttribute() // базовый тип: object
+ public TypeSelectorAttribute(Type type)
+ public TypeSelectorAttribute(params Type[] types)
+ public TypeSelectorAttribute(string assemblyQualifiedName)
+ public TypeSelectorAttribute(params string[] assemblyQualifiedNames)
+
+ public TypeAllow Allow { get; set; } // по умолчанию: TypeAllow.None
+}
+
+[Flags]
+public enum TypeAllow
+{
+ None = 0,
+ Abstract = 1,
+ Interface = 2,
+ All = Abstract | Interface
+}
+```
+
+| Свойство | Описание |
+|----------|----------|
+| `Allow` | Какие специальные категории типов (абстрактные классы, интерфейсы) включаются в список выбора в дополнение к обычным конкретным классам. По умолчанию: `TypeAllow.None` |
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+public class MyBehaviour : MonoBehaviour
+{
+ [TypeSelector(typeof(IMyInterface))]
+ [SerializeField] private string _typeName;
+
+ // Включить абстрактные типы и интерфейсы в список выбора
+ [TypeSelector(typeof(object), Allow = TypeAllow.All)]
+ [SerializeField] private string _anyType;
+}
+```
+
+### Окно выбора типа
+
+В Inspector отображается кнопка, открывающая всплывающее окно с поиском, которое включает:
+
+- Иерархическую организацию по пространствам имён
+- Текстовый поиск с фильтрацией
+- Навигацию с клавиатуры (стрелки, Enter, Escape)
+- Историю навигации (кнопка «назад»)
+- Разрешение неоднозначности для типов с одинаковыми именами из разных сборок
+
+
+---
+
+## Система перечислений
+
+Предоставляет сериализуемые отображения enum → значение, настраиваемые через Inspector.
+
+### EnumValues\
+
+Сериализуемая коллекция записей `EnumValue` с настраиваемым значением по умолчанию. Реализует `IEnumerable>`.
+
+```csharp
+[Serializable]
+public sealed class EnumValues : IEnumerable>
+```
+
+| Член | Описание |
+|------|----------|
+| `TValue GetValue(Enum enumValue)` | Возвращает сопоставленное значение или `_defaultValue`, если не найдено |
+| `bool Equals(Enum, Enum)` | Проверка равенства с поддержкой `[Flags]` |
+
+Поддерживает `[Flags]`-перечисления: `Equals` использует `HasFlag` и корректно обрабатывает члены со значением `0`.
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Enums;
+
+public enum Direction { Left, Right, Up, Down }
+
+public class MyBehaviour : MonoBehaviour
+{
+ [SerializeField] private EnumValues _directionSprites;
+
+ private void SetIcon(Direction dir)
+ {
+ var sprite = _directionSprites.GetValue(dir);
+ _image.sprite = sprite;
+ }
+}
+```
+
+В Inspector выберите тип перечисления в заголовке `EnumValues`, затем назначьте значение для каждого члена перечисления. Нажмите правой кнопкой мыши по свойству, чтобы открыть контекстное меню с пунктом **Populate Missing Enum Members** — он добавит записи для всех отсутствующих членов перечисления, используя текущее Default Value как начальное значение.
+
+---
+
+## Система ID
+
+> **Бета:** Система ID находится в бета-версии. Публичный API, структура генерируемого кода и редакторский UX могут измениться в будущих релизах.
+
+Сопоставляет имя, назначаемое в активе, со стабильным целочисленным ID. Получившийся `int` подходит для `switch` и ключей `Dictionary` без затрат на строковые поиски в рантайме.
+
+Единственный ScriptableObject `IdRegistry` сопоставляет строковые имена стабильным целочисленным ID и предоставляет полные `int ↔ string` поиски в рантайме.
+
+### Использование
+
+**1.** Объявите `partial struct`, реализующий `IId`. Генератор исходников автоматически добавит необходимые поля и свойство:
+
+```csharp
+using Aspid.FastTools.Ids;
+
+public partial struct EnemyId : IId { }
+```
+
+Сгенерированный код:
+
+```csharp
+public partial struct EnemyId
+{
+ [SerializeField] private string __stringId; // editor-only поле, вырезается из player-сборок
+ [SerializeField] private int _id;
+
+ public int Id => _id;
+}
+```
+
+Генератор сообщает `AFID001`, если у структуры отсутствует `partial`, и `AFID002`, если вы сами объявили `_id`, `Id` или `__stringId` (генерация пропускается — вы получаете явную ошибку с указанием на структуру вместо CS-ошибки внутри сгенерированного кода). Поддерживаются generic-структуры (`EnemyId`) и generic-контейнеры.
+
+**2.** Создайте ассет реестра и привяжите его к вашему типу структуры в Inspector:
+- `Assets → Create → Aspid → Id Registry`
+
+**3.** Используйте структуру как сериализуемое поле. В Inspector отображается выпадающий список зарегистрированных имён; окно селектора также позволяет создавать новые записи на лету:
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Ids;
+
+[CreateAssetMenu]
+public class EnemyDefinition : ScriptableObject
+{
+ [UniqueId] [SerializeField] private EnemyId _id;
+}
+```
+
+```csharp
+using UnityEngine;
+using Aspid.FastTools.Ids;
+
+public class EnemySpawner : MonoBehaviour
+{
+ [SerializeField] private EnemyId _targetEnemy;
+
+ private void Spawn()
+ {
+ int id = _targetEnemy.Id; // стабильный integer, безопасен для switch / Dictionary
+ }
+}
+```
+
+### UniqueIdAttribute
+
+Помечает поле как требующее уникального значения среди всех ассетов объявляющего типа. Inspector показывает предупреждение, если два ассета используют одинаковый ID.
+
+```csharp
+[Conditional("UNITY_EDITOR")]
+public sealed class UniqueIdAttribute : PropertyAttribute { }
+```
+
+### IdRegistry
+
+`ScriptableObject` из `Aspid.FastTools.Ids`, хранящий записи `(int, string)` и поддерживающий таблицы поиска доступными во рантайме. Каждому имени назначается стабильный, автоинкрементный ID, который не изменяется даже при добавлении или удалении других записей.
+
+| Член | Описание |
+|------|----------|
+| `bool TryGetId(string name, out int id)` | Возвращает `true` и найденный ID; иначе `false` |
+| `bool TryGetName(int id, out string name)` | Возвращает `true` и найденное имя; иначе `false` и `string.Empty` |
+| `bool Contains(int id)` | Зарегистрирован ли ID |
+| `bool Contains(string name)` | Зарегистрировано ли имя |
+| `int Count` | Количество записей |
+| `IReadOnlyList Ids` · `IReadOnlyList IdNames` | Зарегистрированные ID / имена в порядке регистрации |
+| `IEnumerator> GetEnumerator()` | Итерация по парам `(id, name)` |
+
+Реестр наследуется напрямую от `ScriptableObject` и предоставляет генерик-аналог `IdRegistry` (с `T : struct, IId`), добавляющий типизированные перегрузки `Contains(T)` и `TryGetName(T, out string)`. Редактирование — добавление, переименование, удаление записей — выполняется через инспектор реестра и `RegistryEditorCore`, а не через публичный runtime API.
+
+---
+
+## Расширения SerializedProperty
+
+Цепочные расширения над `SerializedProperty` для синхронизации владеющего `SerializedObject`, записи типизированных значений и рефлексии над полем-источником.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+```csharp
+property
+ .Update()
+ .SetVector3(Vector3.up)
+ .SetBool(true)
+ .ApplyModifiedProperties();
+```
+
+Пакет покрывает:
+
+- **Update / Apply** — `Update`, `UpdateIfRequiredOrScript`, `ApplyModifiedProperties`.
+- **Типизированные сеттеры** — `SetValue` (обобщённый диспетчер) и `SetXxx` для `int`/`uint`/`long`/`ulong`/`float`/`double`/`bool`/`string`/`Color`/`Gradient`/`Hash128`/`Rect`/`RectInt`/`Bounds`/`BoundsInt`/`Vector2..4` (и `Vector2/3Int`)/`Quaternion`/`AnimationCurve`/`EntityId` (Unity 6.2+). К каждому идёт парный вариант `SetXxxAndApply`.
+- **Enum-сеттеры** — `SetEnumFlag` и `SetEnumIndex` (каждый + `AndApply`).
+- **Массивы** — `SetArraySize`, `AddArraySize`, `RemoveArraySize` (каждый + `AndApply`).
+- **Ссылки** — `SetManagedReference`, `SetObjectReference`, `SetExposedReference`, а также `SetBoxed` (Unity 6+).
+- **Рефлексионные хелперы** — `GetPropertyType`, `GetMemberInfo`, `GetClassInstance` для разрешения C#-члена и runtime-экземпляра, стоящих за property.
+
+> Полный справочник по методам: [SerializedPropertyExtensions.md](SerializedPropertyExtensions.md)
+
+---
+
+## IMGUI-области разметки
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+Три `ref struct`-области — `VerticalScope`, `HorizontalScope`, `ScrollViewScope` — оборачивают `EditorGUILayout.Begin*` / `End*`. Каждая предоставляет свойство `Rect` и вызывает соответствующий метод `End*` в `Dispose`:
+
+```csharp
+using (VerticalScope.Begin())
+{
+ EditorGUILayout.LabelField("Item 1");
+ EditorGUILayout.LabelField("Item 2");
+}
+
+using (HorizontalScope.Begin())
+{
+ EditorGUILayout.LabelField("Left");
+ EditorGUILayout.LabelField("Right");
+}
+
+var scrollPos = Vector2.zero;
+using (ScrollViewScope.Begin(ref scrollPos))
+{
+ EditorGUILayout.LabelField("Scrollable content");
+}
+```
+
+Получить rect области через перегрузку с `out`-параметром:
+
+```csharp
+using (VerticalScope.Begin(out var rect, GUI.skin.box))
+{
+ EditorGUI.DrawRect(rect, new Color(0, 0, 0, 0.1f));
+ EditorGUILayout.LabelField("Boxed content");
+}
+```
+
+Все перегрузки `Begin` соответствуют сигнатурам `EditorGUILayout.Begin*` (опциональные `GUIStyle`, `GUILayoutOption[]`, параметры scroll view и т.д.).
+
+---
+
+## Расширения VisualElement
+
+Fluent-методы расширения для построения UIToolkit-деревьев в коде. Все методы возвращают `T` (сам элемент) для цепочки вызовов.
+
+```csharp
+using Aspid.FastTools.UIElements; // runtime-расширения
+using Aspid.FastTools.UIElements.Editors; // editor-only расширения (например, AddOpenScriptCommand)
+```
+
+### Краткий справочник
+
+Пакет покрывает:
+
+- **Основные операции с элементом** — имя, видимость, tooltip, user data, picking mode, data source, а также хелперы `AddChild`/`InsertChild`.
+- **Фокус** — `SetFocus`, `SetBlur`, `SetTabIndex`, `SetFocusable`.
+- **USS** — `AddClass`/`RemoveClass`/`ToggleInClass`/`EnableInClass`, `AddStyleSheets[FromResource]`.
+- **Стили** — все свойства `IStyle`: разметка, размер, отступы, шрифт, текст, цвет, рамка, фон, трансформации (вкл. aspect/filter/material с Unity 6.3+), переходы, overflow, slice, cursor.
+- **Специализированные элементы** — `TextElement`, `ITextEdition`, `ITextSelection`, `BaseField`, `BaseBoolField` (Toggle), `INotifyValueChanged` (с опциональными типами `Unity.Mathematics`), `IMixedValueSupport`, `Button`, `Slider`/`BaseSlider`, `ProgressBar`, `HelpBox`, `Foldout`, `Image`, `IMGUIContainer`, а также полная поверхность `ListView`/`TreeView`/`MultiColumn*`.
+- **Editor-only команды** — `AddOpenScriptCommand`, `BindTo`/`BindPropertyTo`, `Initialize` для `EnumField`/`EnumFlagsField`, подписка на изменения у `PropertyField`.
+- **USS custom-style helpers** — `ICustomStyle.TryGetByEnum` для парсинга строковых USS-свойств в enum.
+
+> Полный справочник по методам: [VisualElementExtensions.md](VisualElementExtensions.md)
+
+### Полный пример
+
+```csharp
+using UnityEditor;
+using UnityEngine;
+using Aspid.FastTools.Editors; // GetScriptName
+using Aspid.FastTools.UIElements; // runtime-расширения VisualElement
+using Aspid.FastTools.UIElements.Editors; // AddOpenScriptCommand
+using UnityEngine.UIElements;
+
+[CustomEditor(typeof(MyBehaviour))]
+public class MyBehaviourEditor : Editor
+{
+ public override VisualElement CreateInspectorGUI()
+ {
+ const string iconPath = "Editor/MyIcon";
+
+ var scriptName = target.GetScriptName();
+ var dark = new Color(0.15f, 0.15f, 0.15f);
+ var light = new Color(0.75f, 0.75f, 0.75f);
+
+ return new VisualElement()
+ .SetName("Header")
+ .SetBackgroundColor(dark)
+ .SetFlexDirection(FlexDirection.Row)
+ .SetPadding(top: 5, bottom: 5, left: 10, right: 10)
+ .SetBorderRadius(topLeft: 10, topRight: 10, bottomLeft: 10, bottomRight: 10)
+ .AddChild(new Image()
+ .SetName("Icon")
+ .AddOpenScriptCommand(target)
+ .SetImageFromResource(iconPath)
+ .SetSize(width: 40, height: 40))
+ .AddChild(new Label(scriptName)
+ .SetName("Title")
+ .SetFlexGrow(1)
+ .SetFontSize(16)
+ .SetMargin(left: 10)
+ .SetColor(light)
+ .SetAlignSelf(Align.Center)
+ .SetOverflow(Overflow.Hidden)
+ .SetWhiteSpace(WhiteSpace.NoWrap)
+ .SetTextOverflow(TextOverflow.Ellipsis)
+ .SetUnityFontStyleAndWeight(FontStyle.Bold));
+ }
+}
+```
+
+### Результат
+
+
+
+---
+
+## Вспомогательные расширения для редактора
+
+Утилитарные методы для получения отображаемых имён объектов Unity в пользовательских редакторах.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+```csharp
+public static string GetScriptName(this Object obj)
+```
+
+Возвращает отображаемое имя объекта Unity:
+- Если тип имеет `[AddComponentMenu]`, возвращает `ObjectNames.GetInspectorTitle(obj)`
+- В противном случае возвращает `ObjectNames.NicifyVariableName(typeName)`
+
+```csharp
+public static string GetScriptNameWithIndex(this Component targetComponent)
+```
+
+Возвращает отображаемое имя с числовым суффиксом, если на одном GameObject присутствует несколько компонентов одного типа. Например, если прикреплены два компонента `AudioSource`, второй вернёт `"Audio Source (2)"`.
+
+```csharp
+[CustomEditor(typeof(MyBehaviour))]
+public class MyBehaviourEditor : Editor
+{
+ public override VisualElement CreateInspectorGUI()
+ {
+ // "My Behaviour" — или "Custom Name", если присутствует [AddComponentMenu("Custom Name")]
+ var name = target.GetScriptName();
+
+ // "My Behaviour (2)" при наличии второго компонента того же типа
+ var nameWithIndex = ((Component)target).GetScriptNameWithIndex();
+
+ return new Label(name);
+ }
+}
+```
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md.meta
new file mode 100644
index 00000000..a1ba8c40
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 9b6c3feaf2078463b98fab80105c4ed8
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md
new file mode 100644
index 00000000..f97bf986
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md
@@ -0,0 +1,124 @@
+# Расширения SerializedProperty — полный справочник
+
+Цепочные методы расширения над `SerializedProperty` для синхронизации владеющего `SerializedObject`, установки значений и рефлексии над полем-источником.
+
+```csharp
+using Aspid.FastTools.Editors;
+```
+
+Все расширения обобщены по `T : SerializedProperty` и возвращают тот же экземпляр, поэтому вызовы можно свободно объединять в цепочки.
+
+## Update / Apply
+
+Тонкие обёртки над одноимёнными методами `SerializedObject` у `property.serializedObject`.
+
+```csharp
+property
+ .Update()
+ .SetInt(42)
+ .ApplyModifiedProperties();
+```
+
+| Метод | Описание |
+|-------|----------|
+| `Update()` | Вызывает `serializedObject.Update()` |
+| `UpdateIfRequiredOrScript()` | Вызывает `serializedObject.UpdateIfRequiredOrScript()` |
+| `ApplyModifiedProperties()` | Вызывает `serializedObject.ApplyModifiedProperties()` |
+
+## SetValue / SetXxx — типизированные сеттеры
+
+Для каждого поддерживаемого типа существуют четыре варианта:
+
+| Вариант | Поведение |
+|---------|-----------|
+| `SetValue(value)` | Обобщённый диспетчер — выбирает нужный типизированный сеттер по runtime-типу значения, возвращает `property` |
+| `SetValueAndApply(value)` | `SetValue(value)` плюс `ApplyModifiedProperties()` |
+| `SetXxx(value)` | Типизированный сеттер (например, `SetInt`), пишущий в соответствующее поле `SerializedProperty.xxxValue` |
+| `SetXxxAndApply(value)` | `SetXxx(value)` плюс `ApplyModifiedProperties()` |
+
+### Поддерживаемые типы
+
+| Семейство методов | Unity-тип | Примечания |
+|-------------------|-----------|------------|
+| `SetInt` | `int` | |
+| `SetUint` | `uint` | |
+| `SetLong` | `long` | |
+| `SetUlong` | `ulong` | |
+| `SetFloat` | `float` | |
+| `SetDouble` | `double` | |
+| `SetBool` | `bool` | |
+| `SetString` | `string` | |
+| `SetColor` | `Color` | |
+| `SetGradient` | `Gradient` | |
+| `SetHash128` | `Hash128` | |
+| `SetRect` / `SetRectInt` | `Rect` / `RectInt` | |
+| `SetBounds` / `SetBoundsInt` | `Bounds` / `BoundsInt` | |
+| `SetVector2` / `SetVector2Int` | `Vector2` / `Vector2Int` | |
+| `SetVector3` / `SetVector3Int` | `Vector3` / `Vector3Int` | |
+| `SetVector4` | `Vector4` | |
+| `SetQuaternion` | `Quaternion` | |
+| `SetAnimationCurve` | `AnimationCurve` | |
+| `SetEntityId` | `Unity.Entities.EntityId` | Unity 6.2+. Apply-вариант называется `SetEntityIdApply` *(имя метода сохраняет опечатку из исходника: пропущено `And`)* |
+
+### Enum-сеттеры
+
+Значения enum не идут через `SetValue` — используйте явную пару ниже в зависимости от того, является ли поле `[Flags]`-перечислением:
+
+| Метод | Описание |
+|-------|----------|
+| `SetEnumFlag(int)` / `SetEnumFlagAndApply(int)` | Пишет в `enumValueFlag` |
+| `SetEnumIndex(int)` / `SetEnumIndexAndApply(int)` | Пишет в `enumValueIndex` |
+
+### Пример
+
+```csharp
+SerializedProperty property = GetProperty();
+
+// Эквивалентные формы
+property.SetValue(10).ApplyModifiedProperties();
+property.SetValueAndApply(10);
+property.SetInt(10).ApplyModifiedProperties();
+property.SetIntAndApply(10);
+
+// Цепочка из нескольких сеттеров
+property
+ .SetVector3(Vector3.up)
+ .SetBool(true)
+ .ApplyModifiedProperties();
+```
+
+## Операции с массивами
+
+| Метод | Описание |
+|-------|----------|
+| `SetArraySize(int)` / `SetArraySizeAndApply(int)` | Устанавливает `property.arraySize` |
+| `AddArraySize(int = 1)` / `AddArraySizeAndApply(int = 1)` | Увеличивает `arraySize` на указанное количество (по умолчанию `1`) |
+| `RemoveArraySize(int = 1)` / `RemoveArraySizeAndApply(int = 1)` | Уменьшает `arraySize` на указанное количество (по умолчанию `1`) |
+
+## Сеттеры ссылок
+
+| Метод | Описание | Примечания |
+|-------|----------|------------|
+| `SetManagedReference(object)` / `SetManagedReferenceAndApply(object)` | Пишет в `managedReferenceValue` (поле должно быть помечено `[SerializeReference]`) | |
+| `SetObjectReference(Object)` / `SetObjectReferenceAndApply(Object)` | Пишет в `objectReferenceValue` | |
+| `SetExposedReference(Object)` / `SetExposedReferenceAndApply(Object)` | Пишет в `exposedReferenceValue` | |
+| `SetBoxed(object)` / `SetBoxedAndApply(object)` | Пишет в `boxedValue` | Unity 6+ |
+
+## Рефлексионные хелперы
+
+Для drawer-/inspector-кода, которому нужно получить runtime-тип или экземпляр, стоящий за property:
+
+| Метод | Возвращает | Описание |
+|-------|------------|----------|
+| `GetPropertyType()` | `Type` или `null` | Возвращает `FieldType` / `PropertyType` C#-члена, стоящего за property. `null`, если член не удаётся разрешить. |
+| `GetMemberInfo()` | `MemberInfo` или `null` | Находит field/property на классе-владельце, имя которого совпадает с `SerializedProperty.name`. Обходит базовые классы через `TypeExtensions.GetMembersInfosIncludingBaseClasses`. |
+| `GetClassInstance()` | `object` | Идёт по `propertyPath` от корневого `targetObject` и возвращает runtime-экземпляр, который непосредственно содержит это property. Поддерживает вложенные объекты, массивы и `List`-поля. |
+
+```csharp
+public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
+{
+ var declaringType = property.GetPropertyType();
+ var owner = property.GetClassInstance();
+ // …
+}
+```
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md.meta
new file mode 100644
index 00000000..76e5ed29
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/SerializedPropertyExtensions.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bf2b6a386bf354961a0a7bb99ff06b10
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md
new file mode 100644
index 00000000..92e5d2a9
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md
@@ -0,0 +1,600 @@
+# Расширения VisualElement — полный справочник
+
+Fluent-методы расширения для построения UIToolkit-деревьев в коде. Все методы возвращают `T` (сам элемент) для цепочки вызовов.
+
+```csharp
+using Aspid.FastTools.UIElements; // runtime-расширения
+using Aspid.FastTools.UIElements.Editors; // editor-only расширения (например, AddOpenScriptCommand)
+```
+
+## Основные операции с элементами
+
+```csharp
+element
+ .SetName("MyElement")
+ .SetVisible(true)
+ .SetTooltip("Текст подсказки")
+ .AddChild(new Label("Hello"))
+ .AddChildren(child1, child2, child3);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetName(string)` | Устанавливает `element.name` |
+| `SetVisible(bool)` | Устанавливает `element.visible` |
+| `SetTooltip(string)` | Устанавливает `element.tooltip` |
+| `SetUserData(object)` | Устанавливает `element.userData` |
+| `SetEnabledSelf(bool)` | Устанавливает `element.enabledSelf` |
+| `SetPickingMode(PickingMode)` | Устанавливает `element.pickingMode` |
+| `SetUsageHints(UsageHints)` | Устанавливает `element.usageHints` |
+| `SetViewDataKey(string)` | Устанавливает `element.viewDataKey` |
+| `SetLanguageDirection(LanguageDirection)` | Устанавливает `element.languageDirection` |
+| `SetDisablePlayModeTint(bool)` | Устанавливает `element.disablePlayModeTint` |
+| `SetDataSource(object)` | Устанавливает `element.dataSource` |
+| `SetDataSourceType(Type)` | Устанавливает `element.dataSourceType` |
+| `SetDataSourcePath(PropertyPath)` | Устанавливает `element.dataSourcePath` |
+| `AddChild(VisualElement)` | Добавляет дочерний элемент, возвращает родителя |
+| `AddChildren(params VisualElement[])` | Добавляет несколько дочерних элементов |
+| `AddChildren(IEnumerable)` | Добавляет из последовательности |
+| `AddChildren(List)` | Добавляет из списка |
+| `AddChildren(Span)` | Добавляет из span |
+| `AddChildren(ReadOnlySpan)` | Добавляет из read-only span |
+| `InsertChild(int, VisualElement)` | Вставляет дочерний элемент по указанному индексу |
+| `InsertChildren(int, params VisualElement[])` | Вставляет несколько дочерних элементов начиная с индекса |
+| `InsertChildren(int, IEnumerable)` | Вставляет из последовательности |
+| `InsertChildren(int, List)` | Вставляет из списка |
+| `InsertChildren(int, Span)` | Вставляет из span |
+| `InsertChildren(int, ReadOnlySpan)` | Вставляет из read-only span |
+
+> `RegisterCallbackOnce` и `RegisterCallbackOnce` доступны на всех версиях Unity (пакет содержит polyfill для версий до 2023.1).
+
+## Focusable
+
+| Метод | Описание |
+|-------|----------|
+| `SetFocus()` | Устанавливает фокус на элемент |
+| `SetBlur()` | Снимает фокус с элемента |
+| `IsFocus()` | Возвращает, находится ли элемент в фокусе |
+| `SetTabIndex(int)` | Устанавливает `element.tabIndex` |
+| `SetFocusable(bool)` | Устанавливает `element.focusable` |
+| `SetDelegatesFocus(bool)` | Устанавливает `element.delegatesFocus` |
+
+## USS и операции с классами
+
+| Метод | Описание |
+|-------|----------|
+| `AddClass(string)` | Добавляет USS-класс |
+| `RemoveClass(string)` | Удаляет USS-класс |
+| `ClearClasses()` | Удаляет все USS-классы |
+| `ToggleInClass(string)` | Переключает USS-класс вкл/выкл |
+| `EnableInClass(string, bool)` | Добавляет или удаляет USS-класс по условию |
+| `AddStyleSheets(StyleSheet)` | Добавляет `StyleSheet` |
+| `RemoveStyleSheets(StyleSheet)` | Удаляет `StyleSheet` |
+| `AddStyleSheetsFromResource(string)` | Добавляет таблицу стилей через `Resources.Load` |
+| `RemoveStyleSheetsFromResource(string)` | Удаляет таблицу стилей, загруженную через `Resources.Load` |
+
+## Расширения стилей — по категориям
+
+Все методы стилей также доступны напрямую на `IStyle` (те же имена методов, работают с объектом стиля).
+
+### Разметка
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetFlexBasis(StyleLength)` | `flexBasis` |
+| `SetFlexGrow(StyleFloat)` | `flexGrow` |
+| `SetFlexShrink(StyleFloat)` | `flexShrink` |
+| `SetFlexWrap(StyleEnum)` | `flexWrap` |
+| `SetFlexDirection(FlexDirection)` | `flexDirection` |
+| `SetAlignSelf(StyleEnum)` | `alignSelf` |
+| `SetAlignItems(StyleEnum)` | `alignItems` |
+| `SetAlignContent(StyleEnum)` | `alignContent` |
+| `SetJustifyContent(StyleEnum)` | `justifyContent` |
+| `SetPosition(StyleEnum)` | `position` |
+
+### Размер
+
+| Метод | Описание |
+|-------|----------|
+| `SetSize(StyleLength)` | Устанавливает ширину и высоту одновременно |
+| `SetSize(width?, height?)` | Устанавливает ширину и/или высоту независимо |
+| `SetMinSize(StyleLength)` | Устанавливает minWidth и minHeight одновременно |
+| `SetMinSize(width?, height?)` | |
+| `SetMaxSize(StyleLength)` | Устанавливает maxWidth и maxHeight одновременно |
+| `SetMaxSize(width?, height?)` | |
+| `SetWidth(StyleLength)` | `width` |
+| `SetMinWidth(StyleLength)` | `minWidth` |
+| `SetMaxWidth(StyleLength)` | `maxWidth` |
+| `SetHeight(StyleLength)` | `height` |
+| `SetMinHeight(StyleLength)` | `minHeight` |
+| `SetMaxHeight(StyleLength)` | `maxHeight` |
+
+### Отступы
+
+Все методы отступов имеют перегрузку с единым значением, перегрузку по сторонам (`top`, `right`, `bottom`, `left`), сеттеры по одной стороне и сеттеры по парам осей X/Y.
+
+| Метод | Свойства стиля |
+|-------|----------------|
+| `SetMargin(…)` / `SetPadding(…)` / `SetDistance(…)` | `Top/Right/Bottom/Left` (общее значение или per-side) |
+| `SetMarginX/Y` · `SetPaddingX/Y` · `SetDistanceX/Y` | Устанавливает горизонтальную (X = `Left`+`Right`) или вертикальную (Y = `Top`+`Bottom`) пару |
+| `SetMarginTop/Right/Bottom/Left` | Margin одной стороны |
+| `SetPaddingTop/Right/Bottom/Left` | Padding одной стороны |
+| `SetDistanceTop/Right/Bottom/Left` *(через `SetTop` / `SetRight` / `SetBottom` / `SetLeft`)* | Смещение одной стороны для абсолютного позиционирования (свойства `top` / `right` / `bottom` / `left`) |
+
+> `SetDistance` — обёртка для четырёх свойств `top`/`right`/`bottom`/`left`, используемых при абсолютном позиционировании. `SetTop`, `SetRight`, `SetBottom`, `SetLeft` — это прямые алиасы для одного свойства.
+
+### Шрифт
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetUnityFont(StyleFont)` | `unityFont` |
+| `SetFontSize(StyleLength)` | `fontSize` |
+| `SetUnityFontDefinition(StyleFontDefinition)` | `unityFontDefinition` |
+| `SetUnityFontStyleAndWeight(StyleEnum)` | `unityFontStyleAndWeight` |
+
+### Пресеты стиля шрифта
+
+Удобные методы для переключения bold / italic без перезаписи другого флага:
+
+| Метод | Описание |
+|-------|----------|
+| `SetNormalUnityFontStyleAndWeight()` | Сбрасывает в `FontStyle.Normal` |
+| `AddBoldUnityFontStyleAndWeight()` | Добавляет bold, сохраняя italic |
+| `RemoveBoldUnityFontStyleAndWeight()` | Убирает bold, сохраняя italic |
+| `AddItalicUnityFontStyleAndWeight()` | Добавляет italic, сохраняя bold |
+| `RemoveItalicUnityFontStyleAndWeight()` | Убирает italic, сохраняя bold |
+
+### Текст
+
+| Метод | Свойство стиля | Примечания |
+|-------|---------------|------------|
+| `SetWorldSpacing(StyleLength)` | `wordSpacing` | |
+| `SetLetterSpacing(StyleLength)` | `letterSpacing` | |
+| `SetUnityTextAlign(TextAnchor)` | `unityTextAlign` | |
+| `SetTextShadow(StyleTextShadow)` | `textShadow` | |
+| `SetUnityTextOutlineColor(StyleColor)` | `unityTextOutlineColor` | |
+| `SetUnityTextOutlineWidth(StyleFloat)` | `unityTextOutlineWidth` | |
+| `SetUnityParagraphSpacing(StyleLength)` | `unityParagraphSpacing` | |
+| `SetTextOverflow(StyleEnum)` | `textOverflow` | |
+| `SetUnityTextOverflowPosition(TextOverflowPosition)` | `unityTextOverflowPosition` | |
+| `SetUnityTextGenerator(TextGeneratorType)` | `unityTextGenerator` | Unity 6+ |
+| `SetUnityEditorTextRenderingMode(EditorTextRenderingMode)` | `unityEditorTextRenderingMode` | Unity 6+ |
+| `SetUnityTextAutoSize(StyleTextAutoSize)` | `unityTextAutoSize` | Unity 6.2+ |
+| `SetWhiteSpace(StyleEnum)` | `whiteSpace` | |
+
+### Цвет и прозрачность
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetColor(StyleColor)` | `color` |
+| `SetColor(string)` | `color`, разобранный из HTML-строки (`"#RRGGBB"` или именованный цвет) |
+| `SetOpacity(StyleFloat)` | `opacity` |
+
+### Рамка
+
+| Метод | Описание |
+|-------|----------|
+| `SetBorderColor(StyleColor)` | Все стороны |
+| `SetBorderColor(top?, right?, bottom?, left?)` | По стороне |
+| `SetBorderColorX(StyleColor)` · `SetBorderColorY(StyleColor)` | Горизонтальная (left + right) или вертикальная (top + bottom) пара |
+| `SetBorderColorTop/Right/Bottom/Left(StyleColor)` | Одна сторона |
+| `SetBorderRadius(StyleLength)` | Все углы |
+| `SetBorderRadius(topLeft?, topRight?, bottomLeft?, bottomRight?)` | По углу |
+| `SetBorderRadiusTop(StyleLength)` · `SetBorderRadiusBottom(StyleLength)` | Пара верхних или нижних углов |
+| `SetBorderRadiusTopLeft/TopRight/BottomLeft/BottomRight(StyleLength)` | Один угол |
+| `SetBorderWidth(StyleFloat)` | Все стороны |
+| `SetBorderWidth(top?, right?, bottom?, left?)` | По стороне |
+| `SetBorderWidthX(StyleFloat)` · `SetBorderWidthY(StyleFloat)` | Горизонтальная или вертикальная пара |
+| `SetBorderWidthTop/Right/Bottom/Left(StyleFloat)` | Одна сторона |
+
+### Фон
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetBackgroundColor(StyleColor)` | `backgroundColor` |
+| `SetBackgroundColor(string)` | `backgroundColor`, разобранный из HTML-строки (`"#RRGGBB"` или именованный цвет) |
+| `SetBackgroundImage(StyleBackground)` | `backgroundImage` |
+| `SetBackgroundImageFromResource(string)` | Загружает `Texture2D` через `Resources.Load` и присваивает его в `backgroundImage` |
+| `SetBackgroundSize(StyleBackgroundSize)` | `backgroundSize` |
+| `SetBackgroundRepeat(StyleBackgroundRepeat)` | `backgroundRepeat` |
+| `SetBackgroundPosition(StyleBackgroundPosition)` | X и Y одновременно |
+| `SetBackgroundPosition(x?, y?)` | Независимо |
+| `SetBackgroundPositionX(StyleBackgroundPosition)` | `backgroundPositionX` |
+| `SetBackgroundPositionY(StyleBackgroundPosition)` | `backgroundPositionY` |
+| `SetUnityBackgroundImageTintColor(StyleColor)` | `unityBackgroundImageTintColor` |
+
+### Трансформации
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetScale(StyleScale)` | `scale` |
+| `SetRotate(StyleRotate)` | `rotate` |
+| `SetTranslate(StyleTranslate)` | `translate` |
+| `SetTransformOrigin(StyleTransformOrigin)` | `transformOrigin` |
+
+### Aspect, Filter и Material
+
+Доступно начиная с Unity 6000.3+.
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetAspectRation(StyleRatio)` | `aspectRatio` *(имя метода сохраняет опечатку из исходника)* |
+| `SetFilter(StyleList)` | `filter` |
+| `SetUnityMaterial(StyleMaterialDefinition)` | `unityMaterial` |
+
+### Анимации переходов
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetTransitionDelay(StyleList)` | `transitionDelay` |
+| `SetTransitionDuration(StyleList)` | `transitionDuration` |
+| `SetTransitionProperty(StyleList)` | `transitionProperty` |
+| `SetTransitionTimingFunction(StyleList)` | `transitionTimingFunction` |
+
+### Переполнение и видимость
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetOverflow(StyleEnum)` | `overflow` |
+| `SetUnityOverflowClipBox(StyleEnum)` | `unityOverflowClipBox` |
+| `SetVisibility(StyleEnum)` | `visibility` |
+| `SetDisplay(DisplayStyle)` | `display` |
+
+### Unity Slice
+
+| Метод | Описание |
+|-------|----------|
+| `SetUnitySlice(StyleInt)` | Все стороны |
+| `SetUnitySlice(top?, right?, bottom?, left?)` | По стороне |
+| `SetUnitySliceX(StyleInt)` · `SetUnitySliceY(StyleInt)` | Горизонтальная (left + right) или вертикальная (top + bottom) пара |
+| `SetUnitySliceTop/Right/Bottom/Left(StyleInt)` | Одна сторона |
+| `SetUnitySliceScale(StyleFloat)` | `unitySliceScale` |
+| `SetUnitySliceType(StyleEnum)` | Unity 6+ |
+
+### Курсор
+
+| Метод | Свойство стиля |
+|-------|----------------|
+| `SetCursor(StyleCursor)` | `cursor` |
+
+## Расширения для специализированных элементов
+
+### TextElement
+
+```csharp
+label
+ .SetText("Hello World")
+ .SetEnableRichText(true)
+ .SetParseEscapeSequences(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetText(string)` | Устанавливает отображаемый текст |
+| `SetEnableRichText(bool)` | Включает разбор тегов rich-text |
+| `SetEmojiFallbackSupport(bool)` | Включает emoji-fallback при рендеринге |
+| `SetParseEscapeSequences(bool)` | Обрабатывать ли escape-последовательности (например, `\n`) |
+| `SetDisplayTooltipWhenElided(bool)` | Показывать обрезанный текст в подсказке при наведении |
+
+### ITextEdition (TextField, IntegerField, …)
+
+```csharp
+textField
+ .SetPlaceholder("Поиск…")
+ .SetMaxLength(64)
+ .SetIsDelayed(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetMaxLength(int)` | Максимальное число символов |
+| `SetMaskChar(char)` | Символ для маскировки пароля |
+| `SetIsDelayed(bool)` | Откладывает изменение значения до потери фокуса / Enter |
+| `SetIsReadOnly(bool)` | Запрещает редактирование |
+| `SetIsPassword(bool)` | Включает password-режим (использует mask char) |
+| `SetPlaceholder(string)` | Текст-плейсхолдер для пустого поля |
+| `SetAutoCorrection(bool)` | Включает автокоррекцию (mobile) |
+| `SetHideMobileInput(bool)` | Скрывает мобильный soft input |
+| `SetHideSoftKeyboard(bool)` | Скрывает экранную клавиатуру |
+| `SetHidePlaceholderOnFocus(bool)` | Убирает плейсхолдер при фокусе |
+| `SetKeyboardType(TouchScreenKeyboardType)` | Тип touch-screen клавиатуры |
+
+### ITextSelection
+
+```csharp
+textField
+ .SetIsSelectable(true)
+ .SetSelectAllOnFocus(true)
+ .AddOnCursorIndexChange(() => Debug.Log(textField.cursorIndex));
+```
+
+| Метод | Описание |
+|-------|----------|
+| `AddOnCursorIndexChange(Action)` / `RemoveOnCursorIndexChange(Action)` | Подписка на изменение позиции курсора |
+| `AddOnSelectIndexChange(Action)` / `RemoveOnSelectIndexChange(Action)` | Подписка на изменение якоря выделения |
+| `SetCursorIndex(int)` | Текущая позиция курсора |
+| `SetSelectIndex(int)` | Текущий якорь выделения |
+| `SetIsSelectable(bool)` | Можно ли выделять текст |
+| `SetSelectAllOnFocus(bool)` | Выделять весь текст при фокусе |
+| `SetSelectAllOnMouseUp(bool)` | Выделять весь текст при отпускании мыши |
+| `SetDoubleClickSelectsWord(bool)` | Двойной клик выделяет слово |
+| `SetTripleClickSelectsLine(bool)` | Тройной клик выделяет строку |
+
+### BaseField\
+
+```csharp
+field.SetLabel("My Field");
+field.SetValue(42);
+```
+
+### BaseBoolField (Toggle)
+
+```csharp
+toggle
+ .SetLabel("Включено")
+ .SetText("Показать расширенные настройки")
+ .SetToggleOnLabelClick(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetText(string)` | Устанавливает текст рядом с чекбоксом |
+| `SetLabel(string)` | Устанавливает label поля |
+| `SetToggleOnLabelClick(bool)` | Переключать ли значение по клику на label |
+
+### INotifyValueChanged\
+
+```csharp
+field.SetValue(42, notify: false); // устанавливает значение без генерации ChangeEvent
+field.AddValueChanged(evt => Debug.Log(evt.newValue));
+field.RemoveValueChanged(myCallback);
+```
+
+Типизированные перегрузки доступны для `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID`, `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, `object`, плюс обобщённый fallback `SetValue`.
+
+> При установленном пакете `com.unity.mathematics` автоматически выставляется define `ASPID_FASTTOOLS_UNITY_MATHEMATICS_INTEGRATION` и добавляются перегрузки `SetValue` / `AddValueChanged` / `RemoveValueChanged` для `int2/3/4` (и `intMxN`), `float2/3/4` (и `floatMxN`), `bool2/3/4` (и `boolMxN`), а также `quaternion`.
+
+### IMixedValueSupport
+
+```csharp
+field.SetShowMixedValue(true); // показывает индикатор смешанного значения
+```
+
+### Button
+
+```csharp
+button
+ .AddClicked(() => Debug.Log("Clicked"))
+ .SetClickable(new Clickable(() => { }))
+ .SetIconImage(myBackground);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `AddClicked(Action)` | Подписка на `Button.clicked` |
+| `RemoveClicked(Action)` | Отписка от `Button.clicked` |
+| `SetClickable(Clickable)` | Устанавливает `Button.clickable` |
+| `SetIconImage(Background)` | Устанавливает `Button.iconImage` |
+
+### Slider / BaseSlider\
+
+```csharp
+slider
+ .SetLowValue(0f)
+ .SetHighValue(100f)
+ .SetShowInputField(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetLowValue(TValue)` | Устанавливает минимальное значение слайдера |
+| `SetHighValue(TValue)` | Устанавливает максимальное значение слайдера |
+| `SetFill(bool)` | Заполнение трека до текущего значения |
+| `SetInverted(bool)` | Инвертирует направление слайдера |
+| `SetPageSize(float)` | Шаг изменения значения при постраничной навигации |
+| `SetShowInputField(bool)` | Показывает числовое поле ввода рядом со слайдером |
+| `SetDirection(SliderDirection)` | Устанавливает ориентацию слайдера |
+
+### ProgressBar
+
+```csharp
+progressBar.SetTitle("Загрузка...").SetLowValue(0f).SetHighValue(100f);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetTitle(string)` | Устанавливает заголовок, отображаемый в центре |
+| `SetLowValue(float)` | Устанавливает минимальное значение |
+| `SetHighValue(float)` | Устанавливает максимальное значение |
+
+### HelpBox
+
+```csharp
+helpBox
+ .SetText("Что-то пошло не так")
+ .SetMessageType(HelpBoxMessageType.Warning);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetText(string)` | Текст сообщения help-box |
+| `SetMessageType(HelpBoxMessageType)` | Иконка / уровень (`None` / `Info` / `Warning` / `Error`) |
+
+### Foldout
+
+```csharp
+foldout
+ .SetText("Section Title")
+ .SetToggleOnLabelClick(true)
+ .SetValue(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetText(string)` | Заголовок foldout |
+| `SetToggleOnLabelClick(bool)` | Переключать ли раскрытие по клику на заголовок |
+
+### Image
+
+```csharp
+image
+ .SetImage(myTexture)
+ .SetTintColor(Color.white)
+ .SetScaleMode(ScaleMode.ScaleToFit);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetImage(Texture)` | Устанавливает `Image.image` |
+| `SetImageFromResource(string)` | Загружает текстуру через `Resources.Load` |
+| `SetSprite(Sprite)` | Устанавливает `Image.sprite` |
+| `SetSpriteFromResource(string)` | Загружает sprite через `Resources.Load` |
+| `SetVectorImage(VectorImage)` | Устанавливает `Image.vectorImage` |
+| `SetVectorImageFromResource(string)` | Загружает vector image через `Resources.Load` |
+| `SetUv(Rect)` | Устанавливает UV-rect |
+| `SetSourceRect(Rect)` | Устанавливает source rect |
+| `SetTintColor(Color)` | Цветовой tint изображения |
+| `SetScaleMode(ScaleMode)` | Режим масштабирования |
+
+### IMGUIContainer
+
+```csharp
+container
+ .SetOnGUIHandler(() => GUILayout.Label("IMGUI"))
+ .SetCullingEnabled(true);
+```
+
+| Метод | Описание |
+|-------|----------|
+| `SetOnGUIHandler(Action)` | Заменяет коллбэк `onGUIHandler` |
+| `AddOnGUIHandler(Action)` | Подписка на `onGUIHandler` |
+| `RemoveOnGUIHandler(Action)` | Отписка от `onGUIHandler` |
+| `SetCullingEnabled(bool)` | Пропускает `onGUIHandler`, когда элемент за пределами экрана |
+| `SetContextType(ContextType)` | Устанавливает тип контекста IMGUI |
+
+### Collection-views (ListView, TreeView, MultiColumn-варианты)
+
+Общие методы распределены по нескольким специализированным расширениям:
+
+- `BaseVerticalCollectionViewExtensions` — применяется ко **всем** collection-views (ListView, TreeView, MultiColumn-варианты).
+- `BaseListViewExtensions` — применяется к ListView и MultiColumnListView.
+- `BaseTreeViewExtensions` — применяется к TreeView и MultiColumnTreeView.
+- `ListViewExtensions` / `TreeViewExtensions` — фабрики `MakeItem`/`BindItem`/`UnbindItem`/`DestroyItem` для своего вью.
+- `MultiColumnListViewExtensions` / `MultiColumnTreeViewExtensions` — хелперы для multi-column-вариантов.
+
+```csharp
+listView
+ .SetItemsSource(items)
+ .SetMakeItem(() => new Label())
+ .SetBindItem((el, i) => ((Label)el).SetText(items[i]))
+ .SetSelectionType(SelectionType.Single)
+ .AddSelectionChanged(selected => Debug.Log(selected));
+```
+
+#### Источник, layout и поведение — `BaseVerticalCollectionView`
+
+| Метод | Описание | Примечания |
+|-------|----------|------------|
+| `SetItemsSource(IList)` | Источник данных | |
+| `SetReorderable(bool)` | Включает drag-reorder | |
+| `SetSelectedIndex(int)` | Выбирает элемент по индексу | |
+| `SetSelectionType(SelectionType)` | None / Single / Multiple | |
+| `SetFixedItemHeight(float)` | Фиксированная высота элемента (для виртуализации `FixedHeight`) | |
+| `SetVirtualizationMethod(CollectionVirtualizationMethod)` | `FixedHeight` или `DynamicHeight` | |
+| `SetHorizontalScrollingEnabled(bool)` | Включает горизонтальную прокрутку | |
+| `SetShowAlternatingRowBackgrounds(AlternatingRowBackground)` | Режим зебра-полос | |
+| `SetMakeFooter(Func)` · `AddMakeFooter` · `RemoveMakeFooter` | Фабрика подвала | Unity 6+ |
+| `SetMakeHeader(Func)` · `AddMakeHeader` · `RemoveMakeHeader` | Фабрика заголовка | Unity 6+ |
+| `SetMakeNoneElement(Func)` · `AddMakeNoneElement` · `RemoveMakeNoneElement` | Фабрика empty-state | Unity 6+ |
+
+#### События — `BaseVerticalCollectionView`
+
+| Метод | Описание |
+|-------|----------|
+| `AddItemsChosen(Action>)` / `RemoveItemsChosen` | Подтверждение элементов (двойной клик / Enter) |
+| `AddSelectionChanged(Action>)` / `RemoveSelectionChanged` | Изменение выделения (объекты) |
+| `AddSelectedIndicesChanged(Action>)` / `RemoveSelectedIndicesChanged` | Изменение выделения (индексы) |
+| `AddItemIndexChanged(Action)` / `RemoveItemIndexChanged` | Перемещение элемента (drag-reorder) |
+| `AddItemsSourceChanged(Action)` / `RemoveItemsSourceChanged` | Смена ссылки `itemsSource` |
+| `AddCanStartDrag(Func)` / `RemoveCanStartDrag` | Кастомный gating старта drag |
+| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Подготовка drag-and-drop |
+| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Визуальный режим drag-and-drop |
+| `AddHandleDrop(Func)` / `RemoveHandleDrop` | Обработка drop |
+
+#### Только для `BaseListView`
+
+| Метод | Описание |
+|-------|----------|
+| `SetAllowAdd(bool)` · `SetAllowRemove(bool)` | Включают встроенные кнопки add/remove |
+| `SetHeaderTitle(string)` | Заголовок при включённом foldout-header |
+| `SetShowFoldoutHeader(bool)` | Оборачивает список в `Foldout` |
+| `SetShowAddRemoveFooter(bool)` | Показывает footer с add/remove |
+| `SetShowBoundCollectionSize(bool)` | Поле размера коллекции |
+| `SetReorderMode(ListViewReorderMode)` | `Simple` или `Animated` |
+| `SetBindingSourceSelectionMode(BindingSourceSelectionMode)` | Auto-assign / manual |
+| `SetOnAdd(Action)` · `AddOnAdd` · `RemoveOnAdd` | Кастомный коллбэк add-кнопки |
+| `SetOnRemove(Action)` · `AddOnRemove` · `RemoveOnRemove` | Кастомный коллбэк remove-кнопки |
+| `SetOverridingAddButtonBehavior(Action)` · `AddOverridingAddButtonBehavior` · `RemoveOverridingAddButtonBehavior` | Подменяет дефолтное поведение add |
+| `AddItemsAdded(Action>)` / `RemoveItemsAdded` | Добавление элементов по индексам |
+| `AddItemsRemoved(Action>)` / `RemoveItemsRemoved` | Удаление элементов по индексам |
+
+#### Только для `BaseTreeView`
+
+| Метод | Описание |
+|-------|----------|
+| `SetAutoExpand(bool)` | Авто-разворачивание новых узлов |
+| `AddItemExpandedChanged(Action)` / `RemoveItemExpandedChanged` | Подписка на изменение раскрытия |
+
+#### Item-фабрики `ListView` / `TreeView`
+
+Эти методы дублируются в `ListViewExtensions` и `TreeViewExtensions` (каждое работает со своим типом view).
+
+| Метод | Описание |
+|-------|----------|
+| `SetMakeItem(Func)` · `AddMakeItem` · `RemoveMakeItem` | Фабрика элементов |
+| `SetBindItem(Action)` · `AddBindItem` · `RemoveBindItem` | Привязка элемента |
+| `SetUnbindItem(Action)` · `AddUnbindItem` · `RemoveUnbindItem` | Отвязка элемента |
+| `SetDestroyItem(Action)` · `AddDestroyItem` · `RemoveDestroyItem` | Уничтожение элемента |
+| `SetItemTemplate(VisualTreeAsset)` | UXML-шаблон, по которому строятся элементы |
+
+#### `MultiColumnListView` / `MultiColumnTreeView`
+
+| Метод | Описание |
+|-------|----------|
+| `SetSortingMode(ColumnSortingMode)` | Встроенный режим сортировки заголовка колонки |
+
+## Команды редактора (только для редактора)
+
+```csharp
+using Aspid.FastTools.UIElements.Editors;
+
+image.AddOpenScriptCommand(target);
+// Двойной клик на элемент открывает скрипт 'target' в IDE
+```
+
+| Метод | Цель | Описание |
+|-------|------|----------|
+| `AddOpenScriptCommand(Object)` | `VisualElement` | Регистрирует обработчик двойного клика, открывающий исходный скрипт `MonoBehaviour` / `ScriptableObject` в IDE. |
+| `BindTo(SerializedObject)` | `VisualElement` | Вызывает `BindingExtensions.Bind` на элементе. |
+| `BindTo(SerializedObject, string propertyPath)` | `IBindable` | Устанавливает `bindingPath` и привязывается к указанному `SerializedObject`. |
+| `BindPropertyTo(SerializedProperty)` | `IBindable` | Вызывает `BindingExtensions.BindProperty` для переданного property. |
+| `Initialize(Enum defaultValue, bool includeObsoleteValues = false)` | `EnumField` / `EnumFlagsField` | Инициализирует поле указанным значением enum по умолчанию. |
+| `AddValueChanged(EventCallback)` / `RemoveValueChanged(...)` | `PropertyField` | Подписка / отписка от уведомлений об изменении свойства. |
+
+## USS custom-style helpers (`ICustomStyle`)
+
+```csharp
+using Aspid.FastTools.UIElements;
+
+private static readonly CustomStyleProperty ThemeProperty = new("--aspid-fasttools-prop-theme");
+
+void OnCustomStyleResolved(CustomStyleResolvedEvent evt)
+{
+ if (evt.customStyle.TryGetByEnum(ThemeProperty, out ThemeStyle.Type theme))
+ ApplyTheme(theme);
+}
+```
+
+| Метод | Описание |
+|-------|----------|
+| `ICustomStyle.TryGetByEnum(CustomStyleProperty, out T)` | Резолвит USS custom-property со строковым значением и парсит её регистронезависимо как enum `T`. Используется во всех `*Style`-структурах с USS-driven enum (`ThemeStyle`, `StatusStyle`, `AspidLabelSizeStyle` и т. д.). |
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md.meta
new file mode 100644
index 00000000..33dbcc05
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Documentation/RU/VisualElementExtensions.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a181f7da19b70437eb557c0cda2dad2a
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues.meta
similarity index 77%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues.meta
index a72d9f7d..2580f461 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c6490710e847c4157a38f045d5205648
+guid: 7c561c66bd9a74b5b910a51cbf1bcca3
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs.meta
similarity index 67%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs.meta
index f833786b..d27664cf 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs.meta
@@ -1,5 +1,6 @@
fileFormatVersion: 2
-guid: 55b0a716a25414d609e45c84c5e34851
+guid: e9d7c8b6a5f4e3d2c1b0a9f8e7d6c5b4
+folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab
new file mode 100644
index 00000000..3b8c25cf
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab
@@ -0,0 +1,112 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1234567890123456789
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2234567890123456789}
+ - component: {fileID: 3234567890123456789}
+ m_Layer: 0
+ m_Name: DamageDealer
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2234567890123456789
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3234567890123456789
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 3bd9644f273549748da7b083e4de1b37, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.EnumValues::Aspid.FastTools.Samples.EnumValues.DamageDealer
+ _damageMultipliers:
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _defaultValue: 1
+ _values:
+ - _key: Physical
+ _value: 1
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Fire
+ _value: 1.5
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Ice
+ _value: 0.8
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Poison
+ _value: 0.6
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _damageColors:
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _defaultValue: {r: 1, g: 1, b: 1, a: 1}
+ _values:
+ - _key: Physical
+ _value: {r: 0.85, g: 0.85, b: 0.85, a: 1}
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Fire
+ _value: {r: 1, g: 0.5, b: 0.1, a: 1}
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Ice
+ _value: {r: 0.4, g: 0.8, b: 1, a: 1}
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Poison
+ _value: {r: 0.5, g: 0.9, b: 0.3, a: 1}
+ _enumType: Aspid.FastTools.Samples.EnumValues.DamageType, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _speedMultipliersByStatus:
+ _enumType: Aspid.FastTools.Samples.EnumValues.StatusEffect, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _defaultValue: 1
+ _values:
+ - _key: Burning, Slowed
+ _value: 0.4
+ _enumType: Aspid.FastTools.Samples.EnumValues.StatusEffect, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Burning
+ _value: 1
+ _enumType: Aspid.FastTools.Samples.EnumValues.StatusEffect, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Frozen
+ _value: 0.2
+ _enumType: Aspid.FastTools.Samples.EnumValues.StatusEffect, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - _key: Slowed
+ _value: 0.5
+ _enumType: Aspid.FastTools.Samples.EnumValues.StatusEffect, Aspid.FastTools.Samples.EnumValues,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _currentDamageType: 1
+ _activeEffects: 5
+ _baseDamage: 10
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/Types/Prefabs/TypeSelectorTest.prefab.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab.meta
similarity index 74%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/Types/Prefabs/TypeSelectorTest.prefab.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab.meta
index cfdabfd5..78d6e5a7 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/Types/Prefabs/TypeSelectorTest.prefab.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Prefabs/EnumValues.prefab.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: d6fe21a6dc34048eea8586d6afdbd9f2
+guid: b6af73ff1ec54f22b2b637c93cf4287b
PrefabImporter:
externalObjects: {}
userData:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md
new file mode 100644
index 00000000..f080c2ee
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md
@@ -0,0 +1,21 @@
+# EnumValues Sample
+
+A tiny combat damage system that maps enum members to typed values through `EnumValues`. `DamageDealer` picks a `DamageType` and `StatusEffect` in the Inspector, then on `Space` applies damage — pulling the damage multiplier, log color, and speed modifier from three `EnumValues` fields.
+
+Look at:
+- `Scripts/DamageDealer.cs:9` — `EnumValues` mapping `DamageType` to damage multiplier.
+- `Scripts/DamageDealer.cs:10` — `EnumValues` mapping `DamageType` to debug log color.
+- `Scripts/DamageDealer.cs:14` — `EnumValues` keyed on `[Flags]` enum `StatusEffect`; composite entries like `Burning | Slowed` must come before single-flag entries (see the inline comment).
+- `Scripts/DamageDealer.cs:30` — `GetValue` call on the Flags-keyed field.
+- `Scripts/StatusEffect.cs` — `[Flags]` enum used by the third mapping.
+
+## How to run
+
+Open `Scenes/EnumValues.unity` and enter Play Mode. The scene hosts a `DamageDealer` wired up from `Prefabs/EnumValues.prefab`, which is pre-seeded with:
+
+- `_damageMultipliers`: `Physical = 1.0`, `Fire = 1.5`, `Ice = 0.8`, `Poison = 0.6`.
+- `_damageColors`: grey / orange / cyan / acid-green per `DamageType`.
+- `_speedMultipliersByStatus`: `Burning | Slowed = 0.4` **first**, then `Burning = 1.0`, `Frozen = 0.2`, `Slowed = 0.5` — composite-first ordering is what makes the combined flag resolve to `0.4` instead of falling through to the first single-flag match.
+- `_currentDamageType = Fire`, `_activeEffects = Burning | Slowed`, `_baseDamage = 10`.
+
+Press `Space` and the Console prints `Fire hit: 15 dmg (speed mod: 0.40)` in orange. Change the enums in the Inspector (or toggle flags in `_activeEffects`) to see different lookups.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md.meta
new file mode 100644
index 00000000..7aa3b861
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5fb2e3aa6c2a846b4b53ffdd69fccf9b
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md
new file mode 100644
index 00000000..3d47f2d7
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md
@@ -0,0 +1,21 @@
+# Пример EnumValues
+
+Маленькая система боевого урона, которая сопоставляет члены enum типизированным значениям через `EnumValues`. `DamageDealer` выбирает `DamageType` и `StatusEffect` в Inspector, а по нажатию `Space` наносит урон — извлекая множитель урона, цвет лога и модификатор скорости из трёх полей `EnumValues`.
+
+Смотрите:
+- `Scripts/DamageDealer.cs:9` — `EnumValues`, сопоставляющий `DamageType` множителю урона.
+- `Scripts/DamageDealer.cs:10` — `EnumValues`, сопоставляющий `DamageType` цвету отладочного лога.
+- `Scripts/DamageDealer.cs:14` — `EnumValues` по `[Flags]` enum `StatusEffect`; композитные записи вроде `Burning | Slowed` должны идти до одиночных флагов (см. комментарий рядом).
+- `Scripts/DamageDealer.cs:30` — вызов `GetValue` на Flags-поле.
+- `Scripts/StatusEffect.cs` — `[Flags]` enum, используемый в третьем сопоставлении.
+
+## Как запустить
+
+Откройте `Scenes/EnumValues.unity` и войдите в Play Mode. В сцене есть `DamageDealer`, подключённый из `Prefabs/EnumValues.prefab`, который предзаполнен:
+
+- `_damageMultipliers`: `Physical = 1.0`, `Fire = 1.5`, `Ice = 0.8`, `Poison = 0.6`.
+- `_damageColors`: серый / оранжевый / голубой / ядовито-зелёный по `DamageType`.
+- `_speedMultipliersByStatus`: `Burning | Slowed = 0.4` **первой**, затем `Burning = 1.0`, `Frozen = 0.2`, `Slowed = 0.5` — порядок с композитом впереди именно и гарантирует, что комбинация флагов разрешается в `0.4`, а не проваливается на первое совпадение одиночного флага.
+- `_currentDamageType = Fire`, `_activeEffects = Burning | Slowed`, `_baseDamage = 10`.
+
+Нажмите `Space` — в Console появится оранжевое `Fire hit: 15 dmg (speed mod: 0.40)`. Меняйте значения enum в Inspector (или переключайте флаги в `_activeEffects`), чтобы увидеть другие варианты поиска.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md.meta
new file mode 100644
index 00000000..cb485b44
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/README_RU.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 7a3b1c4d5e6f7081929304a5b6c7d8e9
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes.meta
similarity index 77%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes.meta
index 78aeed3f..c445a598 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c5527f6f042e7408ebabfc82d150433b
+guid: d68d6fc528e76449db0a639caa762203
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity
new file mode 100644
index 00000000..8eeccc3e
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity
@@ -0,0 +1,449 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 10
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 13
+ m_BakeOnSceneLoad: 0
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 1
+ m_PVRFilteringGaussRadiusAO: 1
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 3
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ buildHeightMesh: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
+--- !u!1 &100001
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 100003}
+ - component: {fileID: 100002}
+ - component: {fileID: 100004}
+ m_Layer: 0
+ m_Name: Directional Light
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!108 &100002
+Light:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ m_Enabled: 1
+ serializedVersion: 13
+ m_Type: 1
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
+ m_Intensity: 1
+ m_Range: 10
+ m_SpotAngle: 30
+ m_InnerSpotAngle: 21.80208
+ m_CookieSize2D: {x: 10, y: 10}
+ m_Shadows:
+ m_Type: 2
+ m_Resolution: -1
+ m_CustomResolution: -1
+ m_Strength: 1
+ m_Bias: 0.05
+ m_NormalBias: 0.4
+ m_NearPlane: 0.2
+ m_CullingMatrixOverride:
+ e00: 1
+ e01: 0
+ e02: 0
+ e03: 0
+ e10: 0
+ e11: 1
+ e12: 0
+ e13: 0
+ e20: 0
+ e21: 0
+ e22: 1
+ e23: 0
+ e30: 0
+ e31: 0
+ e32: 0
+ e33: 1
+ m_UseCullingMatrixOverride: 0
+ m_Cookie: {fileID: 0}
+ m_DrawHalo: 0
+ m_Flare: {fileID: 0}
+ m_RenderMode: 0
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingLayerMask: 1
+ m_Lightmapping: 4
+ m_LightShadowCasterMode: 0
+ m_AreaSize: {x: 1, y: 1}
+ m_BounceIntensity: 1
+ m_ColorTemperature: 6570
+ m_UseColorTemperature: 0
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_UseBoundingSphereOverride: 0
+ m_UseViewFrustumForShadowCasterCull: 1
+ m_ForceVisible: 0
+ m_ShapeRadius: 0
+ m_ShadowAngle: 0
+ m_LightUnit: 1
+ m_LuxAtDistance: 1
+ m_EnableSpotReflector: 1
+--- !u!4 &100003
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
+ m_LocalPosition: {x: 0, y: 3, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
+--- !u!114 &100004
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalLightData
+ m_UsePipelineSettings: 1
+ m_AdditionalLightsShadowResolutionTier: 2
+ m_CustomShadowLayers: 0
+ m_LightCookieSize: {x: 1, y: 1}
+ m_LightCookieOffset: {x: 0, y: 0}
+ m_SoftShadowQuality: 0
+ m_RenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ShadowRenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_Version: 4
+ m_LightLayerMask: 1
+ m_ShadowLayerMask: 1
+ m_RenderingLayers: 1
+ m_ShadowRenderingLayers: 1
+--- !u!1 &200001
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 200004}
+ - component: {fileID: 200002}
+ - component: {fileID: 200003}
+ - component: {fileID: 200005}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!81 &200002
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+--- !u!20 &200003
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 0
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &200004
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 1, z: -10}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &200005
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_TaaSettings:
+ m_Quality: 3
+ m_FrameInfluence: 0.1
+ m_JitterScale: 1
+ m_MipBias: 0
+ m_VarianceClampScale: 0.9
+ m_ContrastAdaptiveSharpening: 0
+ m_Version: 2
+--- !u!1001 &1501333895
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 1234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_Name
+ value: EnumValues
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: b6af73ff1ec54f22b2b637c93cf4287b, type: 3}
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 200004}
+ - {fileID: 100003}
+ - {fileID: 1501333895}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity.meta
new file mode 100644
index 00000000..8d581a24
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scenes/EnumValues.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dbdae9a1d49d4a6ca82c5a3ab5bc1d24
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts.meta
similarity index 67%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts.meta
index c1b162f2..46f29a9c 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts.meta
@@ -1,5 +1,6 @@
fileFormatVersion: 2
-guid: 7f88981b89c7a48f196d36c976c31f20
+guid: d3bd9c0a75f8b4c77986e2aeb360dc13
+folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef
similarity index 76%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef
index 5199cfd0..9e824ff2 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef
@@ -1,8 +1,8 @@
{
- "name": "Aspid.UnityFastTools.ProfilerMarkers",
+ "name": "Aspid.FastTools.Samples.EnumValues",
"rootNamespace": "",
"references": [
- "GUID:7c010b89992542508a6b6189977e64d4"
+ "Aspid.FastTools.Unity"
],
"includePlatforms": [],
"excludePlatforms": [],
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef.meta
new file mode 100644
index 00000000..46d73b13
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/Aspid.FastTools.Samples.EnumValues.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: fe2fa73e3f3d499f8e95516349d3768a
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs
new file mode 100644
index 00000000..957d9800
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs
@@ -0,0 +1,37 @@
+using UnityEngine;
+using Aspid.FastTools.Enums;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.EnumValues
+{
+ public sealed class DamageDealer : MonoBehaviour
+ {
+ [SerializeField] private EnumValues _damageMultipliers;
+ [SerializeField] private EnumValues _damageColors;
+
+ // Flag combinations (e.g. Burning | Slowed) match via HasFlag and first-hit wins, so list
+ // composite entries BEFORE their constituent flags — otherwise the single-flag entry matches first.
+ [SerializeField] private EnumValues _speedMultipliersByStatus;
+
+ [SerializeField] private DamageType _currentDamageType = DamageType.Physical;
+ [SerializeField] private StatusEffect _activeEffects = StatusEffect.None;
+ [SerializeField] private float _baseDamage = 10f;
+
+ private void Update()
+ {
+ if (!Input.GetKeyDown(KeyCode.Space)) return;
+ DealDamage();
+ }
+
+ private void DealDamage()
+ {
+ var multiplier = _damageMultipliers.GetValue(_currentDamageType);
+ var color = _damageColors.GetValue(_currentDamageType);
+ var speedMod = _speedMultipliersByStatus.GetValue(_activeEffects);
+ var finalDamage = _baseDamage * multiplier;
+ var colorHex = ColorUtility.ToHtmlStringRGB(color);
+
+ Debug.Log($"{_currentDamageType} hit: {finalDamage} dmg (speed mod: {speedMod:F2})");
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs.meta
new file mode 100644
index 00000000..2f16782f
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageDealer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3bd9644f273549748da7b083e4de1b37
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs
new file mode 100644
index 00000000..3a42c2fd
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs
@@ -0,0 +1,11 @@
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.EnumValues
+{
+ public enum DamageType
+ {
+ Physical,
+ Fire,
+ Ice,
+ Poison,
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs.meta
new file mode 100644
index 00000000..7b7289d5
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/DamageType.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8617fbe193994a1d9ffd71e02ac3c5b5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs
new file mode 100644
index 00000000..8922e133
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs
@@ -0,0 +1,17 @@
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.EnumValues
+{
+ [Flags]
+ public enum StatusEffect
+ {
+ None = 0,
+ Burning = 1,
+ Frozen = 2,
+ Slowed = 4,
+ Stunned = 8,
+ // Combinations such as Burning | Slowed are matched via HasFlag semantics in EnumValues
+ // and can be registered as their own entry with a dedicated value.
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs.meta
new file mode 100644
index 00000000..52cb56ff
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/EnumValues/Scripts/StatusEffect.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9f66e403cd7044cc96c00ad003ccc68a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids.meta
new file mode 100644
index 00000000..d3271a04
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5ba755d592364471fa27430d0b3b2299
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data.meta
new file mode 100644
index 00000000..81bb9e92
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aabb1122334455667788990011223344
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset
new file mode 100644
index 00000000..f117b9f8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset
@@ -0,0 +1,23 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 70e390d14588e72409a6a95aca1461a7, type: 3}
+ m_Name: IdRegistry_EnemyId
+ m_EditorClassIdentifier: Aspid.FastTools.Unity::Aspid.FastTools.Ids.IdRegistry
+ _targetStructType: Aspid.FastTools.Samples.Ids.EnemyId, Aspid.FastTools.Samples.Ids,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _nextId: 5
+ _ids: 01000000020000000300000004000000
+ _names:
+ - fly_enemy_dragon
+ - walk_enemy_goblin
+ - walk_enemy_orc
+ - walk_enemy_skeleton
diff --git a/Aspid.UnityFastTools/Assets/Settings/Renderer/URP Global Settings.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset.meta
old mode 100755
new mode 100644
similarity index 79%
rename from Aspid.UnityFastTools/Assets/Settings/Renderer/URP Global Settings.asset.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset.meta
index 81b84f2a..ae394d4f
--- a/Aspid.UnityFastTools/Assets/Settings/Renderer/URP Global Settings.asset.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/IdRegistry_EnemyId.asset.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 18dc0cd2c080841dea60987a38ce93fa
+guid: 3b547b6a17aa54f5f8c80dd0aa23d0e2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset
new file mode 100644
index 00000000..ba1dde9a
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset
@@ -0,0 +1,20 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 953e77fd8a034ac6974fb9fa4c49aae4, type: 3}
+ m_Name: fly_enemy_dragon
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Ids::Aspid.FastTools.Samples.Ids.EnemyDefinition
+ _id:
+ __stringId: fly_enemy_dragon
+ _id: 1
+ _displayName: Dragon
+ _maxHealth: 500
+ _moveSpeed: 4
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset.meta
new file mode 100644
index 00000000..5fa2fe1e
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/fly_enemy_dragon.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: edca3936962f4004af0272178ad75bfd
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset
new file mode 100644
index 00000000..c258690b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset
@@ -0,0 +1,20 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 953e77fd8a034ac6974fb9fa4c49aae4, type: 3}
+ m_Name: walk_enemy_goblin
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Ids::Aspid.FastTools.Samples.Ids.EnemyDefinition
+ _id:
+ __stringId: walk_enemy_goblin
+ _id: 2
+ _displayName: Goblin
+ _maxHealth: 80
+ _moveSpeed: 3.5
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset.meta
new file mode 100644
index 00000000..1a107503
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_goblin.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d92adbaedb324bff9fcbe616fbf03416
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset
new file mode 100644
index 00000000..91665de9
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset
@@ -0,0 +1,20 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 953e77fd8a034ac6974fb9fa4c49aae4, type: 3}
+ m_Name: walk_enemy_orc
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Ids::Aspid.FastTools.Samples.Ids.EnemyDefinition
+ _id:
+ __stringId: walk_enemy_orc
+ _id: 3
+ _displayName: Orc
+ _maxHealth: 150
+ _moveSpeed: 2
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset.meta
new file mode 100644
index 00000000..5c039b9d
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_orc.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fc61e5b486184ad4b3ed8c54016f6d5f
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset
new file mode 100644
index 00000000..cbf097dc
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset
@@ -0,0 +1,20 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 953e77fd8a034ac6974fb9fa4c49aae4, type: 3}
+ m_Name: walk_enemy_skeleton
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Ids::Aspid.FastTools.Samples.Ids.EnemyDefinition
+ _id:
+ __stringId: walk_enemy_skeleton
+ _id: 4
+ _displayName: Skeleton
+ _maxHealth: 60
+ _moveSpeed: 3
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset.meta
new file mode 100644
index 00000000..0de4d09b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Data/walk_enemy_skeleton.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 56ba102dc1764873b64e5f7803207cc6
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs.meta
new file mode 100644
index 00000000..8b71e2ab
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bbcc1122334455667788990011223344
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab
new file mode 100644
index 00000000..79633111
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab
@@ -0,0 +1,54 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1234567890123456789
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2234567890123456789}
+ - component: {fileID: 3234567890123456789}
+ m_Layer: 0
+ m_Name: Ids
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2234567890123456789
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3234567890123456789
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: ce0efc593ee24a5d97d8511d669c9de7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Ids::Aspid.FastTools.Samples.Ids.EnemySpawner
+ _catalog:
+ - {fileID: 11400000, guid: edca3936962f4004af0272178ad75bfd, type: 2}
+ - {fileID: 11400000, guid: d92adbaedb324bff9fcbe616fbf03416, type: 2}
+ - {fileID: 11400000, guid: fc61e5b486184ad4b3ed8c54016f6d5f, type: 2}
+ - {fileID: 11400000, guid: 56ba102dc1764873b64e5f7803207cc6, type: 2}
+ _spawnTarget:
+ __stringId: walk_enemy_orc
+ _id: 3
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab.meta
new file mode 100644
index 00000000..059c2697
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Prefabs/Ids.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bbe54125698a41cb8a8dab897d567ff8
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md
new file mode 100644
index 00000000..84c30649
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md
@@ -0,0 +1,30 @@
+# Ids Sample
+
+Demonstrates the `IId` / `IdRegistry` / `[UniqueId]` trio: fields show a human-readable string in the Inspector while serializing as a stable integer, and the Inspector catches collisions at edit-time.
+
+## How it works
+
+- `IId` — a marker interface declaring the `int Id { get; }` property.
+- `IdRegistry` — a `ScriptableObject` that binds a struct type to a list of `(Id, Name)` entries and keeps the name ↔ int map available at runtime. The property drawer renders a dropdown sourced from this registry.
+- `[UniqueId]` — validates at edit-time that no two `ScriptableObject` assets share the same resolved integer ID.
+
+## Scenario
+
+An enemy catalog. Each `EnemyDefinition` asset holds a unique `EnemyId` plus display data (`_displayName`, `_maxHealth`, `_moveSpeed`). An `EnemySpawner` picks a target `EnemyId` via dropdown and looks the matching asset up in its catalog on `Start()`.
+
+Look at:
+
+- `Scripts/EnemyId.cs` — `partial struct : IId`. `IdStructGenerator` emits `__stringId`, `_id`, and the `Id` property.
+- `Scripts/EnemyDefinition.cs:10` — `[UniqueId]` on a serialized `EnemyId` field prevents duplicate IDs across assets.
+- `Data/IdRegistry_EnemyId.asset` — the registry binding names (`fly_enemy_dragon`, `walk_enemy_goblin`, `walk_enemy_orc`, `walk_enemy_skeleton`) to stable ints.
+- `Scripts/EnemySpawner.cs:9` — dropdown-selected `EnemyId` resolved to `int` at runtime via `.Id`.
+
+## How to run
+
+Open `Scenes/Ids.unity` — it contains an `EnemySpawner` GameObject (also available as `Prefabs/Ids.prefab`). Wire it up once:
+
+1. Drag the four `Data/*_enemy_*.asset` files into the spawner's `Catalog` array.
+2. Pick a target enemy from the `Spawn Target` dropdown — the picker is sourced from `IdRegistry_EnemyId`.
+3. Enter Play Mode — the Console logs the resolved `EnemyDefinition` (display name, HP, move speed). Switch the dropdown to see different lookups.
+
+To create more entries, open `Data/IdRegistry_EnemyId.asset` to add registry rows, then `Assets > Create > Aspid > FastTools > Samples > Enemy Definition` for the asset side.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md.meta
new file mode 100644
index 00000000..55d313e2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 79a0f34c9d7ac4d45a00d0f1a9909de4
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md
new file mode 100644
index 00000000..4cacef15
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md
@@ -0,0 +1,30 @@
+# Пример Ids
+
+Демонстрирует связку `IId` / `IdRegistry` / `[UniqueId]`: поля показывают человекочитаемую строку в Inspector, а сериализуются как стабильное целое число, и Inspector ловит коллизии прямо при редактировании.
+
+## Как это работает
+
+- `IId` — маркерный интерфейс, объявляющий свойство `int Id { get; }`.
+- `IdRegistry` — `ScriptableObject`, связывающий тип-структуру со списком записей `(Id, Name)` и сохраняющий отображение имя ↔ int доступным во рантайме. Property drawer отрисовывает выпадающий список, источником которого является этот реестр.
+- `[UniqueId]` — валидирует во время редактирования, что ни два `ScriptableObject`-актива не имеют одинакового результирующего целочисленного ID.
+
+## Сценарий
+
+Каталог врагов. Каждый актив `EnemyDefinition` хранит уникальный `EnemyId` плюс данные для отображения (`_displayName`, `_maxHealth`, `_moveSpeed`). `EnemySpawner` выбирает целевой `EnemyId` через выпадающий список и ищет соответствующий актив в своём каталоге в `Start()`.
+
+Смотрите:
+
+- `Scripts/EnemyId.cs` — `partial struct : IId`. `IdStructGenerator` генерирует `__stringId`, `_id` и свойство `Id`.
+- `Scripts/EnemyDefinition.cs:10` — `[UniqueId]` на сериализованном поле `EnemyId` предотвращает дублирование ID между активами.
+- `Data/IdRegistry_EnemyId.asset` — реестр, связывающий имена (`fly_enemy_dragon`, `walk_enemy_goblin`, `walk_enemy_orc`, `walk_enemy_skeleton`) со стабильными целочисленными значениями.
+- `Scripts/EnemySpawner.cs:9` — выбранный из списка `EnemyId`, преобразуется в `int` во время выполнения через `.Id`.
+
+## Как запустить
+
+Откройте `Scenes/Ids.unity` — в сцене лежит GameObject `EnemySpawner` (также доступен как `Prefabs/Ids.prefab`). Подключите его один раз:
+
+1. Перетащите четыре актива `Data/*_enemy_*.asset` в массив `Catalog` спавнера.
+2. Выберите цель в выпадающем списке `Spawn Target` — список берётся из `IdRegistry_EnemyId`.
+3. Войдите в Play Mode — в Console появится лог найденного `EnemyDefinition` (display name, HP, move speed). Меняйте значение в списке, чтобы увидеть другие варианты поиска.
+
+Чтобы добавить новые записи, откройте `Data/IdRegistry_EnemyId.asset` для строк реестра и `Assets > Create > Aspid > FastTools > Samples > Enemy Definition` — для соответствующих ассетов.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md.meta
new file mode 100644
index 00000000..bdd6dddf
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/README_RU.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ad6e4f7081924b3c5d6e7f80910a1b2c
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes.meta
new file mode 100644
index 00000000..9ba291b0
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ccdd1122334455667788990011223344
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity
similarity index 75%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity
index 42337b6b..4731c200 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scenes/Visual Elements.unity
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity
@@ -119,7 +119,7 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
---- !u!1 &1956111
+--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -127,9 +127,9 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 1956113}
- - component: {fileID: 1956112}
- - component: {fileID: 1956114}
+ - component: {fileID: 100003}
+ - component: {fileID: 100002}
+ - component: {fileID: 100004}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
@@ -137,22 +137,22 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!108 &1956112
+--- !u!108 &100002
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
m_Enabled: 1
- serializedVersion: 11
+ serializedVersion: 13
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
- m_CookieSize: 10
+ m_CookieSize2D: {x: 10, y: 10}
m_Shadows:
m_Type: 2
m_Resolution: -1
@@ -197,18 +197,18 @@ Light:
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
- m_ShadowRadius: 0
+ m_ShapeRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
---- !u!4 &1956113
+--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
@@ -217,13 +217,13 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
---- !u!114 &1956114
+--- !u!114 &100004
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
@@ -246,7 +246,7 @@ MonoBehaviour:
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
---- !u!1 &628555577
+--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -254,10 +254,10 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 628555580}
- - component: {fileID: 628555579}
- - component: {fileID: 628555578}
- - component: {fileID: 628555581}
+ - component: {fileID: 200004}
+ - component: {fileID: 200002}
+ - component: {fileID: 200003}
+ - component: {fileID: 200005}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -265,21 +265,21 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!81 &628555578
+--- !u!81 &200002
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
---- !u!20 &628555579
+--- !u!20 &200003
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
@@ -324,13 +324,13 @@ Camera:
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
---- !u!4 &628555580
+--- !u!4 &200004
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
@@ -339,13 +339,13 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!114 &628555581
+--- !u!114 &200005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
@@ -383,54 +383,67 @@ MonoBehaviour:
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
---- !u!1 &642957226
-GameObject:
+--- !u!1001 &1455822910
+PrefabInstance:
m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 642957227}
- - component: {fileID: 642957228}
- m_Layer: 0
- m_Name: Visual Element Inspector
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &642957227
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 642957226}
serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -0, y: 0, z: -0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!114 &642957228
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 642957226}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: dcfbfa43e527425e8881c33eec85bec3, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp-firstpass::Aspid.UnityFastTools.Samples.VisualElements.VisualElementInspector
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 1234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_Name
+ value: Ids
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: bbe54125698a41cb8a8dab897d567ff8, type: 3}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- - {fileID: 628555580}
- - {fileID: 1956113}
- - {fileID: 642957227}
+ - {fileID: 200004}
+ - {fileID: 100003}
+ - {fileID: 1455822910}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity.meta
new file mode 100644
index 00000000..bad758d2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scenes/Ids.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a978c0327ba8450696b7a546a1c26959
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts.meta
new file mode 100644
index 00000000..3926b383
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4b48604dab5254cd296df004a49949ff
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Source/Runtime/Aspid.UnityFastTools.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef
similarity index 74%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Source/Runtime/Aspid.UnityFastTools.asmdef
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef
index f0f15e45..113ba780 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Source/Runtime/Aspid.UnityFastTools.asmdef
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef
@@ -1,7 +1,9 @@
{
- "name": "Aspid.UnityFastTools",
+ "name": "Aspid.FastTools.Samples.Ids",
"rootNamespace": "",
- "references": [],
+ "references": [
+ "Aspid.FastTools.Unity"
+ ],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef.meta
new file mode 100644
index 00000000..a62fec60
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/Aspid.FastTools.Samples.Ids.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 34984631d7b44f53aa977da146924369
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs
new file mode 100644
index 00000000..7910e72b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs
@@ -0,0 +1,22 @@
+using UnityEngine;
+using Aspid.FastTools.Ids;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Ids
+{
+ [CreateAssetMenu(fileName = "New Enemy Definition", menuName = "Aspid/FastTools/Samples/Enemy Definition")]
+ public class EnemyDefinition : ScriptableObject
+ {
+ [UniqueId]
+ [SerializeField] private EnemyId _id;
+
+ [SerializeField] private string _displayName;
+ [SerializeField] private int _maxHealth;
+ [SerializeField] private float _moveSpeed;
+
+ public EnemyId Id => _id;
+ public string DisplayName => _displayName;
+ public int MaxHealth => _maxHealth;
+ public float MoveSpeed => _moveSpeed;
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs.meta
new file mode 100644
index 00000000..43009abb
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 953e77fd8a034ac6974fb9fa4c49aae4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs
new file mode 100644
index 00000000..632905fd
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs
@@ -0,0 +1,11 @@
+using System;
+using Aspid.FastTools.Ids;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Ids
+{
+ // Declaring `partial struct : IId` triggers IdStructGenerator, which emits __stringId, _id,
+ // and the Id property — so consumers only ever write the one-liner below.
+ [Serializable]
+ public partial struct EnemyId : IId { }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs.meta
new file mode 100644
index 00000000..9b790019
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemyId.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1d2651027ff84fa5a6f13b1bc0ab8978
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs
new file mode 100644
index 00000000..52e3c67b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs
@@ -0,0 +1,24 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Ids
+{
+ public class EnemySpawner : MonoBehaviour
+ {
+ [SerializeField] private EnemyDefinition[] _catalog;
+ [SerializeField] private EnemyId _spawnTarget;
+
+ private void Start()
+ {
+ foreach (var enemy in _catalog)
+ {
+ if (enemy.Id.Id != _spawnTarget.Id) continue;
+
+ Debug.Log($"Spawning {enemy.DisplayName} — HP: {enemy.MaxHealth}, Speed: {enemy.MoveSpeed}");
+ return;
+ }
+
+ Debug.LogWarning($"No EnemyDefinition found for id {_spawnTarget.Id}");
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs.meta
new file mode 100644
index 00000000..4729ced3
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Ids/Scripts/EnemySpawner.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ce0efc593ee24a5d97d8511d669c9de7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs.meta
new file mode 100644
index 00000000..9f9d9055
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 67e55e4b150b9426293f6e52bf919fdd
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab
new file mode 100644
index 00000000..d4c819db
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab
@@ -0,0 +1,46 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1234567890123456789
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2234567890123456789}
+ - component: {fileID: 3234567890123456789}
+ m_Layer: 0
+ m_Name: FrameProfiler
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2234567890123456789
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3234567890123456789
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 81773b495ca84416af8f5b4210f650f6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.ProfilerMarkers::Aspid.FastTools.Samples.ProfilerMarkers.FrameProfiler
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab.meta
new file mode 100644
index 00000000..3370b575
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Prefabs/ProfilerMarkers.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4d438180f96141d9b383946bbf647c1c
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md
new file mode 100644
index 00000000..098fb274
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md
@@ -0,0 +1,61 @@
+# ProfilerMarkers Sample
+
+Demonstrates the three supported usage forms of `this.Marker()`. In every
+form the Aspid.FastTools source generator replaces each call site with a
+unique `ProfilerMarker` keyed by `(type, method, line)`. `.WithName(...)`
+is optional — when omitted, the generator auto-names the marker after the
+enclosing type and method.
+
+## Supported forms
+
+1. **Block `using`-statement with an explicit name.** Marker scope is the
+ block; the full display name is `"{TypeName}.{WithName} ({line})"`, so
+ pass the short suffix only.
+ ```csharp
+ using (this.Marker().WithName("Physics")) // Profiler: "FrameProfiler.Physics ()"
+ SimulatePhysics();
+ ```
+2. **`using`-declaration without `WithName`.** Marker scope is the rest of
+ the enclosing block; the generator auto-names the marker after the
+ enclosing method.
+ ```csharp
+ using var _ = this.Marker(); // Profiler: "FrameProfiler.SimulateInput ()"
+ ```
+3. **Combined form.** A method-wide `using`-declaration paired with a
+ nested `using`-statement — useful when you want one outer marker for the
+ whole method and a narrower marker around a hot sub-step. Both get
+ auto-named after the method; their different line numbers produce
+ distinct Profiler entries.
+ ```csharp
+ using var _ = this.Marker(); // Profiler: "FrameProfiler.SimulateAudio ()"
+ using (this.Marker()) // Profiler: "FrameProfiler.SimulateAudio ()"
+ {
+ // Some code.
+ }
+ ```
+
+## How to run
+
+1. Open `Scenes/ProfilerMarkers.unity` — it already contains a `FrameProfiler`
+ GameObject. The `Prefabs/ProfilerMarkers.prefab` variant can be dropped
+ into your own scenes.
+2. Open `Window → Analysis → Profiler`.
+3. Enter Play Mode and inspect the CPU track for the named markers.
+
+## Where to look
+
+- `Scripts/FrameProfiler.cs:19,22,25` — three top-level markers with explicit
+ names in `Update` (`FrameProfiler.Physics`, `FrameProfiler.AI`,
+ `FrameProfiler.Render`).
+- `Scripts/FrameProfiler.cs:44` — nested `FrameProfiler.AI.Agent` marker
+ emitted once per loop iteration in `SimulateAI`, appearing under the `AI`
+ scope.
+- `Scripts/FrameProfiler.cs:68` — `using`-declaration form without `WithName`
+ in `SimulateInput`; the generator names the marker after the method.
+- `Scripts/FrameProfiler.cs:84,87` — combined form in `SimulateAudio`: an
+ outer method-wide `using`-declaration plus a nested `using`-statement
+ around `MixAudio()`.
+
+Every `using` scope (statement or declaration) starts and ends the
+generated marker automatically, so the Profiler shows precise self-time
+for every phase.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md.meta
new file mode 100644
index 00000000..95a29bc6
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b168656911e1546e5b8b5036803240be
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md
new file mode 100644
index 00000000..554a9549
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md
@@ -0,0 +1,61 @@
+# Пример ProfilerMarkers
+
+Демонстрирует три поддерживаемые формы вызова `this.Marker()`. В каждой
+форме source-генератор Aspid.FastTools заменяет место вызова уникальным
+`ProfilerMarker`, привязанным к `(type, method, line)`. `.WithName(...)`
+не обязателен — если его не указать, генератор автоматически назовёт
+маркер по имени содержащего типа и метода.
+
+## Поддерживаемые формы
+
+1. **Блок `using`-statement с явным именем.** Область маркера — блок;
+ полное отображаемое имя имеет вид `"{TypeName}.{WithName} ({line})"`,
+ поэтому в `WithName` передавайте только короткий суффикс.
+ ```csharp
+ using (this.Marker().WithName("Physics")) // Profiler: "FrameProfiler.Physics ()"
+ SimulatePhysics();
+ ```
+2. **`using`-declaration без `WithName`.** Область маркера — остаток
+ содержащего блока; генератор именует маркер по имени содержащего
+ метода.
+ ```csharp
+ using var _ = this.Marker(); // Profiler: "FrameProfiler.SimulateInput ()"
+ ```
+3. **Комбинированная форма.** Метод-широкий `using`-declaration в паре с
+ вложенным `using`-statement — удобно, когда нужен один внешний маркер
+ на весь метод и более узкий — на «горячий» подэтап. Оба маркера
+ получают автоимя по методу; разные номера строк дают разные записи в
+ Profiler.
+ ```csharp
+ using var _ = this.Marker(); // Profiler: "FrameProfiler.SimulateAudio ()"
+ using (this.Marker()) // Profiler: "FrameProfiler.SimulateAudio ()"
+ {
+ // Некоторый код.
+ }
+ ```
+
+## Как запустить
+
+1. Откройте `Scenes/ProfilerMarkers.unity` — в сцене уже есть GameObject с
+ `FrameProfiler`. Вариант `Prefabs/ProfilerMarkers.prefab` можно добавить
+ в собственные сцены.
+2. Откройте `Window → Analysis → Profiler`.
+3. Войдите в Play Mode и осмотрите CPU-трек на наличие именованных маркеров.
+
+## Где смотреть
+
+- `Scripts/FrameProfiler.cs:19,22,25` — три маркера верхнего уровня с явными
+ именами в `Update` (`FrameProfiler.Physics`, `FrameProfiler.AI`,
+ `FrameProfiler.Render`).
+- `Scripts/FrameProfiler.cs:44` — вложенный маркер `FrameProfiler.AI.Agent`
+ в `SimulateAI`, генерируемый раз за итерацию цикла, отображается под
+ областью `AI`.
+- `Scripts/FrameProfiler.cs:68` — форма `using`-declaration без `WithName`
+ в `SimulateInput`; генератор именует маркер по имени метода.
+- `Scripts/FrameProfiler.cs:84,87` — комбинированная форма в
+ `SimulateAudio`: внешний метод-широкий `using`-declaration плюс
+ вложенный `using`-statement вокруг `MixAudio()`.
+
+Каждый блок `using` (statement или declaration) автоматически запускает и
+завершает сгенерированный маркер, поэтому Profiler показывает точное
+self-time для каждой фазы.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md.meta
new file mode 100644
index 00000000..91185974
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/README_RU.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 9c5d3e6f70819203b4c5d6e7f8091a2b
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes.meta
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity
similarity index 89%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity
index 404b9673..750900c1 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scenes/Profiler Markers.unity
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity
@@ -119,7 +119,7 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
---- !u!1 &1956111
+--- !u!1 &100001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -127,9 +127,9 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 1956113}
- - component: {fileID: 1956112}
- - component: {fileID: 1956114}
+ - component: {fileID: 100003}
+ - component: {fileID: 100002}
+ - component: {fileID: 100004}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
@@ -137,22 +137,22 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!108 &1956112
+--- !u!108 &100002
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
m_Enabled: 1
- serializedVersion: 11
+ serializedVersion: 13
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
- m_CookieSize: 10
+ m_CookieSize2D: {x: 10, y: 10}
m_Shadows:
m_Type: 2
m_Resolution: -1
@@ -197,18 +197,18 @@ Light:
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
- m_ShadowRadius: 0
+ m_ShapeRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
---- !u!4 &1956113
+--- !u!4 &100003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
@@ -217,17 +217,17 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
---- !u!114 &1956114
+--- !u!114 &100004
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1956111}
+ m_GameObject: {fileID: 100001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
- m_Name:
+ m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalLightData
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
@@ -246,7 +246,7 @@ MonoBehaviour:
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
---- !u!1 &628555577
+--- !u!1 &200001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -254,10 +254,10 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 628555580}
- - component: {fileID: 628555579}
- - component: {fileID: 628555578}
- - component: {fileID: 628555581}
+ - component: {fileID: 200004}
+ - component: {fileID: 200002}
+ - component: {fileID: 200003}
+ - component: {fileID: 200005}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -265,21 +265,21 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!81 &628555578
+--- !u!81 &200002
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
---- !u!20 &628555579
+--- !u!20 &200003
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
@@ -324,13 +324,13 @@ Camera:
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
---- !u!4 &628555580
+--- !u!4 &200004
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
@@ -339,17 +339,17 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!114 &628555581
+--- !u!114 &200005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 628555577}
+ m_GameObject: {fileID: 200001}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
- m_Name:
+ m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
@@ -383,7 +383,7 @@ MonoBehaviour:
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
---- !u!1 &642957226
+--- !u!1 &300001
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -391,46 +391,46 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 642957227}
- - component: {fileID: 642957228}
+ - component: {fileID: 300002}
+ - component: {fileID: 300003}
m_Layer: 0
- m_Name: Marker Test
+ m_Name: FrameProfiler
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!4 &642957227
+--- !u!4 &300002
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 642957226}
+ m_GameObject: {fileID: 300001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -0, y: 0, z: -0}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!114 &642957228
+--- !u!114 &300003
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 642957226}
+ m_GameObject: {fileID: 300001}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 199696d16b85e4112a63975aa9dcfd55, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp-firstpass::Aspid.UnityFastTools.Samples.ProfilerMarkers.MarkerTest
+ m_Script: {fileID: 11500000, guid: 81773b495ca84416af8f5b4210f650f6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.ProfilerMarkers::Aspid.FastTools.Samples.ProfilerMarkers.FrameProfiler
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- - {fileID: 628555580}
- - {fileID: 1956113}
- - {fileID: 642957227}
+ - {fileID: 200004}
+ - {fileID: 100003}
+ - {fileID: 300002}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity.meta
new file mode 100644
index 00000000..7adfa9b3
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scenes/ProfilerMarkers.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 345b39adb7834b58b853bfeee78228bb
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts.meta
similarity index 100%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts.meta
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef
new file mode 100644
index 00000000..092138d3
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "Aspid.FastTools.Samples.ProfilerMarkers",
+ "rootNamespace": "",
+ "references": [
+ "Aspid.FastTools.Unity"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef.meta
new file mode 100644
index 00000000..99365ca0
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/Aspid.FastTools.Samples.ProfilerMarkers.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 240a32c5c53f4b2dafe608de70ef1eb2
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs
new file mode 100644
index 00000000..02b328be
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs
@@ -0,0 +1,107 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.ProfilerMarkers
+{
+ public sealed class FrameProfiler : MonoBehaviour
+ {
+ [SerializeField] [Min(1)] private int _physicsLoad = 5000;
+ [SerializeField] [Min(1)] private int _aiAgents = 20;
+ [SerializeField] [Min(1)] private int _aiStepsPerAgent = 500;
+ [SerializeField] [Min(1)] private int _renderLoad = 3000;
+ [SerializeField] [Min(1)] private int _inputLoad = 1500;
+ [SerializeField] [Min(1)] private int _audioLoad = 2000;
+
+ private void Update()
+ {
+ // Every call site of this.Marker() becomes a unique ProfilerMarker.
+ // Display name: "{TypeName}.{WithName-or-method} ({line})".
+ using (this.Marker().WithName("Physics")) // Profiler: FrameProfiler.Physics (19)
+ SimulatePhysics();
+
+ using (this.Marker().WithName("AI")) // Profiler: FrameProfiler.AI (22)
+ SimulateAI();
+
+ using (this.Marker().WithName("Render")) // Profiler: FrameProfiler.Render (25)
+ SimulateRender();
+
+ SimulateInput();
+ SimulateAudio();
+ }
+
+ private void SimulatePhysics()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _physicsLoad; i++)
+ sum += Mathf.Sqrt(i);
+ _ = sum;
+ }
+
+ private void SimulateAI()
+ {
+ for (var agent = 0; agent < _aiAgents; agent++)
+ {
+ using (this.Marker().WithName("AI.Agent")) // Profiler: FrameProfiler.AI.Agent (44)
+ StepAgent();
+ }
+ }
+
+ private void StepAgent()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _aiStepsPerAgent; i++)
+ sum += Mathf.Sin(i);
+ _ = sum;
+ }
+
+ private void SimulateRender()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _renderLoad; i++)
+ sum += Mathf.Cos(i);
+ _ = sum;
+ }
+
+ // using-declaration without .WithName(): auto-named after the method.
+ private void SimulateInput()
+ {
+ using var _ = this.Marker(); // Profiler: FrameProfiler.SimulateInput (68)
+ DoInputWork();
+ }
+
+ private void DoInputWork()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _inputLoad; i++)
+ sum += Mathf.Tan(i);
+ _ = sum;
+ }
+
+ // Combined form: outer method-wide using-declaration + nested using-statement.
+ // Both auto-named after the method; distinct because their line numbers differ.
+ private void SimulateAudio()
+ {
+ using var _ = this.Marker(); // Profiler: FrameProfiler.SimulateAudio (84)
+ PrepareAudio();
+
+ using (this.Marker()) // Profiler: FrameProfiler.SimulateAudio (87)
+ MixAudio();
+ }
+
+ private void PrepareAudio()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _audioLoad; i++)
+ sum += Mathf.Sqrt(i);
+ _ = sum;
+ }
+
+ private void MixAudio()
+ {
+ var sum = 0f;
+ for (var i = 0; i < _audioLoad; i++)
+ sum += Mathf.Cos(i);
+ _ = sum;
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs.meta
new file mode 100644
index 00000000..209bacc8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/ProfilerMarkers/Scripts/FrameProfiler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 81773b495ca84416af8f5b4210f650f6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types.meta
new file mode 100644
index 00000000..ccc47fc4
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5549194b371a41d08e429360a65e08ff
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs.meta
new file mode 100644
index 00000000..84e9ac9a
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d7c22acfa3b44fdeba05b6ac683bbc1b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab
new file mode 100644
index 00000000..fc0897f2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab
@@ -0,0 +1,56 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1234567890123456789
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2234567890123456789}
+ - component: {fileID: 3234567890123456789}
+ m_Layer: 0
+ m_Name: AbilitySelector
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2234567890123456789
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3234567890123456789
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1234567890123456789}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5c99d520a5f74297b09811a16d0e65fd, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Types::Aspid.FastTools.Samples.Types.AbilitySelector
+ _abilityType:
+ _assemblyQualifiedName: Aspid.FastTools.Samples.Types.Dash, Aspid.FastTools.Samples.Types,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ _modifierTypes:
+ - Aspid.FastTools.Samples.Types.CooldownReductionModifier, Aspid.FastTools.Samples.Types,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - Aspid.FastTools.Samples.Types.DoubleDamageModifier, Aspid.FastTools.Samples.Types,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
+ - Aspid.FastTools.Samples.Types.RangeBoostModifier, Aspid.FastTools.Samples.Types,
+ Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab.meta
new file mode 100644
index 00000000..7a58b3f2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/AbilitySelector.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c922b4c1592c4065b541f02f0acc425b
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab
new file mode 100644
index 00000000..c5d01843
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab
@@ -0,0 +1,47 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &9120107065992179554
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3305627272239008027}
+ - component: {fileID: 9066011880317238500}
+ m_Layer: 0
+ m_Name: Enemy
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3305627272239008027
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 9120107065992179554}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &9066011880317238500
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 9120107065992179554}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0667ffc8ad8b4dbba538545a568350f3, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.Types::Aspid.FastTools.Samples.Types.FastEnemy
+ _health: 100
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab.meta
new file mode 100644
index 00000000..c782e275
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Prefabs/Enemy.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f2ebce908ab18403bbddb4e5101b460a
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md
new file mode 100644
index 00000000..038dca69
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md
@@ -0,0 +1,21 @@
+# Types Sample
+
+A tiny ability system that demonstrates polymorphic type selection in the Unity Inspector using `SerializableType`, `TypeSelectorAttribute`, and `ComponentTypeSelector`. The player picks an `Ability` subclass and a list of `AbilityModifier` subclasses; enemies use `ComponentTypeSelector` so the concrete enemy script can be hot-swapped from the Inspector.
+
+Look at:
+
+- `Scripts/Abilities/AbilitySelector.cs:20` — `SerializableType` field, constrained picker for a single subtype.
+- `Scripts/Abilities/AbilitySelector.cs:25` — `[TypeSelector(typeof(AbilityModifier))]` on a `string[]` field.
+- `Scripts/Enemies/EnemyBase.cs:18` — `ComponentTypeSelector` declaration that swaps the attached script in place.
+
+Both Type drawers ship a UIToolkit and an IMGUI rendering path. Parallel `IMGUI*` variants force the IMGUI path so you can compare them side by side or migrate IMGUI-only projects:
+
+- `Scripts/Abilities/IMGUIAbilitySelector.cs` + `Scripts/Editor/IMGUIAbilitySelectorEditor.cs` — same `SerializableType` / `[TypeSelector]` fields rendered via `OnInspectorGUI`.
+- `Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs` (+ `IMGUIFastEnemy`, `IMGUITankEnemy`) + `Scripts/Editor/IMGUIEnemyBaseEditor.cs` — IMGUI counterpart of the `ComponentTypeSelector` swap flow.
+
+## How to run
+
+Open `Scenes/Types.unity` — it contains two prefab instances:
+
+- **AbilitySelector** (`Prefabs/AbilitySelector.prefab`) — an `AbilitySelector` with `Dash` pre-picked and all three modifiers filled in. Enter Play Mode to see the Console log the activated ability and each applied modifier.
+- **Enemy** (`Prefabs/Enemy.prefab`) — a `FastEnemy` wired up through `ComponentTypeSelector`. Select it in the Hierarchy and use the type dropdown at the top of the Inspector to swap between `FastEnemy` and `TankEnemy` in place; the `Health` field persists across the swap.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md.meta
new file mode 100644
index 00000000..851455a1
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c7e265c2e7c3641f4bfa4ade9da6a940
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md
new file mode 100644
index 00000000..7ebc3355
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md
@@ -0,0 +1,21 @@
+# Пример Types
+
+Маленькая система способностей, демонстрирующая полиморфный выбор типа в Unity Inspector с помощью `SerializableType`, `TypeSelectorAttribute` и `ComponentTypeSelector`. Игрок выбирает наследника `Ability` и список наследников `AbilityModifier`; для врагов используется `ComponentTypeSelector`, чтобы конкретный скрипт врага можно было заменить «на лету» из Inspector.
+
+Смотрите:
+
+- `Scripts/Abilities/AbilitySelector.cs:20` — поле `SerializableType`, ограниченный выбор одного подтипа.
+- `Scripts/Abilities/AbilitySelector.cs:25` — `[TypeSelector(typeof(AbilityModifier))]` на поле `string[]`.
+- `Scripts/Enemies/EnemyBase.cs:18` — объявление `ComponentTypeSelector`, заменяющее прикреплённый скрипт по месту.
+
+Оба Type-drawer’а поддерживают и UIToolkit, и IMGUI. Параллельные `IMGUI*`-варианты принудительно используют IMGUI-путь — удобно для сравнения или миграции IMGUI-проектов:
+
+- `Scripts/Abilities/IMGUIAbilitySelector.cs` + `Scripts/Editor/IMGUIAbilitySelectorEditor.cs` — те же поля `SerializableType` / `[TypeSelector]`, отрисованные через `OnInspectorGUI`.
+- `Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs` (+ `IMGUIFastEnemy`, `IMGUITankEnemy`) + `Scripts/Editor/IMGUIEnemyBaseEditor.cs` — IMGUI-эквивалент потока подмены через `ComponentTypeSelector`.
+
+## Как запустить
+
+Откройте `Scenes/Types.unity` — в сцене два prefab-инстанса:
+
+- **AbilitySelector** (`Prefabs/AbilitySelector.prefab`) — `AbilitySelector` с предвыбранной способностью `Dash` и тремя заполненными модификаторами. Войдите в Play Mode, чтобы увидеть в Console лог активированной способности и каждого применённого модификатора.
+- **Enemy** (`Prefabs/Enemy.prefab`) — `FastEnemy`, подключённый через `ComponentTypeSelector`. Выделите его в Hierarchy и используйте выпадающий список выбора типа в верхней части Inspector, чтобы переключиться между `FastEnemy` и `TankEnemy` по месту; значение `Health` сохраняется при замене.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md.meta
new file mode 100644
index 00000000..2fee21cf
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/README_RU.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8b4c2d5e6f7081923a4b5c6d7e8f9012
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes.meta
new file mode 100644
index 00000000..34e5f9fe
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity
new file mode 100644
index 00000000..dccc62e4
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity
@@ -0,0 +1,507 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 10
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 0
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 13
+ m_BakeOnSceneLoad: 0
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 1
+ m_PVRFilteringGaussRadiusAO: 1
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 3
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ buildHeightMesh: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
+--- !u!1 &100001
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 100003}
+ - component: {fileID: 100002}
+ - component: {fileID: 100004}
+ m_Layer: 0
+ m_Name: Directional Light
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!108 &100002
+Light:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ m_Enabled: 1
+ serializedVersion: 13
+ m_Type: 1
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
+ m_Intensity: 1
+ m_Range: 10
+ m_SpotAngle: 30
+ m_InnerSpotAngle: 21.80208
+ m_CookieSize2D: {x: 10, y: 10}
+ m_Shadows:
+ m_Type: 2
+ m_Resolution: -1
+ m_CustomResolution: -1
+ m_Strength: 1
+ m_Bias: 0.05
+ m_NormalBias: 0.4
+ m_NearPlane: 0.2
+ m_CullingMatrixOverride:
+ e00: 1
+ e01: 0
+ e02: 0
+ e03: 0
+ e10: 0
+ e11: 1
+ e12: 0
+ e13: 0
+ e20: 0
+ e21: 0
+ e22: 1
+ e23: 0
+ e30: 0
+ e31: 0
+ e32: 0
+ e33: 1
+ m_UseCullingMatrixOverride: 0
+ m_Cookie: {fileID: 0}
+ m_DrawHalo: 0
+ m_Flare: {fileID: 0}
+ m_RenderMode: 0
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingLayerMask: 1
+ m_Lightmapping: 4
+ m_LightShadowCasterMode: 0
+ m_AreaSize: {x: 1, y: 1}
+ m_BounceIntensity: 1
+ m_ColorTemperature: 6570
+ m_UseColorTemperature: 0
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_UseBoundingSphereOverride: 0
+ m_UseViewFrustumForShadowCasterCull: 1
+ m_ForceVisible: 0
+ m_ShapeRadius: 0
+ m_ShadowAngle: 0
+ m_LightUnit: 1
+ m_LuxAtDistance: 1
+ m_EnableSpotReflector: 1
+--- !u!4 &100003
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
+ m_LocalPosition: {x: 0, y: 3, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
+--- !u!114 &100004
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 100001}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalLightData
+ m_UsePipelineSettings: 1
+ m_AdditionalLightsShadowResolutionTier: 2
+ m_CustomShadowLayers: 0
+ m_LightCookieSize: {x: 1, y: 1}
+ m_LightCookieOffset: {x: 0, y: 0}
+ m_SoftShadowQuality: 0
+ m_RenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ShadowRenderingLayersMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_Version: 4
+ m_LightLayerMask: 1
+ m_ShadowLayerMask: 1
+ m_RenderingLayers: 1
+ m_ShadowRenderingLayers: 1
+--- !u!1 &200001
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 200004}
+ - component: {fileID: 200002}
+ - component: {fileID: 200003}
+ - component: {fileID: 200005}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!81 &200002
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+--- !u!20 &200003
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 0
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &200004
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 1, z: -10}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &200005
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 200001}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_TaaSettings:
+ m_Quality: 3
+ m_FrameInfluence: 0.1
+ m_JitterScale: 1
+ m_MipBias: 0
+ m_VarianceClampScale: 0.9
+ m_ContrastAdaptiveSharpening: 0
+ m_Version: 2
+--- !u!1001 &970635237
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 3
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3305627272239008027, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 9120107065992179554, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+ propertyPath: m_Name
+ value: Enemy
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: f2ebce908ab18403bbddb4e5101b460a, type: 3}
+--- !u!1001 &1794465240
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: 1234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_Name
+ value: AbilitySelector
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 3
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 2234567890123456789, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: c922b4c1592c4065b541f02f0acc425b, type: 3}
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 200004}
+ - {fileID: 100003}
+ - {fileID: 1794465240}
+ - {fileID: 970635237}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity.meta
new file mode 100644
index 00000000..b944dbca
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scenes/Types.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a551a863bae542399c669640b76d544b
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts.meta
new file mode 100644
index 00000000..6a7f9fbd
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bb5f0e994a694cc68a6ad846fb3c94e1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities.meta
new file mode 100644
index 00000000..444e3905
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5f15b5a12db04b76a37194e14b45df5d
+timeCreated: 1776546281
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs
new file mode 100644
index 00000000..f2de7446
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs
@@ -0,0 +1,10 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public abstract class Ability : MonoBehaviour
+ {
+ public abstract void Activate();
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs.meta
new file mode 100644
index 00000000..260241d9
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Ability.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3f81a50759684a12a0e30c2af6f84e7c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs
new file mode 100644
index 00000000..0b8bbd0b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs
@@ -0,0 +1,55 @@
+using System;
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ // Demonstrates two complementary ways to pick a System.Type from the Inspector:
+ //
+ // 1. SerializableType — strongly typed wrapper, constrained via generic parameter.
+ // Resolution is cached; implicit conversion to Type? is free.
+ // 2. [TypeSelector] + string — annotate a raw string field to get the same picker window.
+ // Useful when you want multiple base constraints
+ // or want to opt out of the wrapper allocation.
+ //
+ // Both forms store the assembly-qualified name and resolve lazily at first access.
+ public sealed class AbilitySelector : MonoBehaviour
+ {
+ // Picker is constrained to Ability subtypes by the generic argument.
+ [SerializeField] private SerializableType _abilityType;
+
+ // Array field + attribute: each element is its own picker constrained to AbilityModifier.
+ // Allow defaults to TypeAllow.None — abstract bases and interfaces are hidden;
+ // set Allow = TypeAllow.Abstract / Interface / All to opt in.
+ [TypeSelector(typeof(AbilityModifier))]
+ [SerializeField] private string[] _modifierTypes;
+
+ private void Start()
+ {
+ // .Type performs the lazy GetType() lookup and caches the result.
+ // Returns null if the stored assembly-qualified name no longer resolves
+ // (e.g., the type was renamed).
+ var abilityType = _abilityType.Type;
+ if (abilityType is null) return;
+
+ // Implicit conversion is guaranteed safe: the picker only offers Ability subtypes.
+ var ability = (Ability)gameObject.AddComponent(abilityType);
+ ability.Activate();
+
+ // Raw-string form: resolve manually via Type.GetType.
+ // Skip silently on unresolved names to keep the sample self-contained —
+ // production code should log or surface the missing type.
+ foreach (var qualifiedName in _modifierTypes)
+ {
+ var modifierType = Type.GetType(qualifiedName);
+ if (modifierType is null) continue;
+
+ // Modifiers are plain C# classes (not components) — Activator.CreateInstance
+ // is appropriate here; AddComponent would fail for non-UnityEngine.Object types.
+ var modifier = (AbilityModifier)Activator.CreateInstance(modifierType);
+ modifier.Apply();
+ }
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs.meta
new file mode 100644
index 00000000..80bc5b0e
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/AbilitySelector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5c99d520a5f74297b09811a16d0e65fd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs
new file mode 100644
index 00000000..10025678
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class Dash : Ability
+ {
+ public override void Activate() =>
+ Debug.Log("Dash performed!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs.meta
new file mode 100644
index 00000000..d5b874da
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Dash.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fd1b54437b9541c1b53f6820cf40e7a2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs
new file mode 100644
index 00000000..627fca26
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class Fireball : Ability
+ {
+ public override void Activate() =>
+ Debug.Log("Fireball launched!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs.meta
new file mode 100644
index 00000000..db60c1da
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Fireball.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f86571f0dfa54afa89d3e98721bc6c1b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs
new file mode 100644
index 00000000..66ca5817
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class Heal : Ability
+ {
+ public override void Activate() =>
+ Debug.Log("Heal cast!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs.meta
new file mode 100644
index 00000000..d3a8bed3
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/Heal.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: df96ad82595246f284ec3aad4a445e6e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs
new file mode 100644
index 00000000..ea85607f
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs
@@ -0,0 +1,28 @@
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ // Demonstrates the IMGUI rendering path for Aspid.FastTools Type drawers.
+ //
+ // The same SerializableType and [TypeSelector] fields used by the UIToolkit-based
+ // AbilitySelector sample are reused here, but the companion editor
+ // (IMGUIAbilitySelectorEditor) overrides OnInspectorGUI without CreateInspectorGUI,
+ // forcing Unity to render the inspector — and every nested Type picker — through
+ // the IMGUI code path (TypeIMGUIPropertyDrawer).
+ //
+ // Useful when migrating projects that still rely on IMGUI editors, or when verifying
+ // that both rendering paths stay visually and behaviourally aligned.
+ public sealed class IMGUIAbilitySelector : MonoBehaviour
+ {
+ // Picker is constrained to Ability subtypes by the generic argument.
+ [SerializeField] private SerializableType _primaryAbility;
+
+ // Array field + attribute: each element is its own picker constrained to AbilityModifier.
+ // Allow defaults to TypeAllow.None — abstract bases and interfaces are hidden;
+ // set Allow = TypeAllow.Abstract / Interface / All to opt in.
+ [TypeSelector(typeof(AbilityModifier))]
+ [SerializeField] private string[] _modifierTypes;
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs.meta
new file mode 100644
index 00000000..a2cff078
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Abilities/IMGUIAbilitySelector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a3f17c8b29d04e7eaa2d9a1c75b8e404
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef
new file mode 100644
index 00000000..b537bd20
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "Aspid.FastTools.Samples.Types",
+ "rootNamespace": "",
+ "references": [
+ "Aspid.FastTools.Unity"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef.meta
new file mode 100644
index 00000000..a57965b8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Aspid.FastTools.Samples.Types.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 56ba1765a6eb4975ada12506d8c4afda
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor.meta
new file mode 100644
index 00000000..89a7efdb
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1d6a8b3f2c45418792e07c8541b9d2e3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef
new file mode 100644
index 00000000..a6f07de1
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef
@@ -0,0 +1,19 @@
+{
+ "name": "Aspid.FastTools.Samples.Types.Editor",
+ "references": [
+ "GUID:56ba1765a6eb4975ada12506d8c4afda",
+ "GUID:7c010b89992542508a6b6189977e64d4",
+ "GUID:94dcbbdbbd3ca48b891ee4fc8455c434"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef.meta
new file mode 100644
index 00000000..20ba97a1
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/Aspid.FastTools.Samples.Types.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f7b30a1e2c5d4ee0b34c1f2a7e9d4068
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs
new file mode 100644
index 00000000..2fad35d7
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs
@@ -0,0 +1,23 @@
+using UnityEditor;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types.Editors
+{
+ // Forces IMGUI rendering for the IMGUIAbilitySelector inspector.
+ //
+ // Unity decides between IMGUI and UIToolkit at the Editor level: when
+ // CreateInspectorGUI is NOT overridden but OnInspectorGUI is, the entire inspector —
+ // including every nested PropertyDrawer — falls back to the IMGUI path. That routes
+ // SerializableType and [TypeSelector] fields through TypeIMGUIPropertyDrawer.OnGUI
+ // instead of CreatePropertyGUI, demonstrating the IMGUI rendering of the picker.
+ [CustomEditor(typeof(IMGUIAbilitySelector))]
+ internal sealed class IMGUIAbilitySelectorEditor : Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+ DrawPropertiesExcluding(serializedObject, "m_Script");
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs.meta
new file mode 100644
index 00000000..08ada244
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIAbilitySelectorEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8e2c44a7b91f4dc9a06d45f7c81b09a2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs
new file mode 100644
index 00000000..1a51790f
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs
@@ -0,0 +1,22 @@
+using UnityEditor;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types.Editors
+{
+ // Forces IMGUI rendering for IMGUIEnemyBase and every concrete subtype.
+ //
+ // editorForChildClasses: true makes this editor apply to IMGUIFastEnemy / IMGUITankEnemy
+ // too, so swapping the component's m_Script via ComponentTypeSelector keeps the inspector
+ // in IMGUI mode after Unity rebuilds the editor for the new subtype. Without that flag,
+ // the post-swap subtype would fall back to the default UIToolkit inspector.
+ [CustomEditor(typeof(IMGUIEnemyBase), editorForChildClasses: true)]
+ internal sealed class IMGUIEnemyBaseEditor : Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+ DrawPropertiesExcluding(serializedObject, "m_Script");
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs.meta
new file mode 100644
index 00000000..9a351121
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Editor/IMGUIEnemyBaseEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9b7e1d2a5c4f48a3a05e7c41b9d28a36
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies.meta
new file mode 100644
index 00000000..b27daeb6
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 7b2295ff4cc94122ad458718856ad4a0
+timeCreated: 1776546314
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs
new file mode 100644
index 00000000..fd906257
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs
@@ -0,0 +1,26 @@
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ // Demonstrates ComponentTypeSelector: a serialized marker that adds an Inspector
+ // dropdown letting you swap this component's script to any subtype of EnemyBase
+ // (FastEnemy, TankEnemy, ...) without removing and re-adding the component.
+ //
+ // The picker auto-discovers subtypes via the field's declaring class — no
+ // configuration needed. Selection rewrites m_Script on the SerializedObject,
+ // so all fields persist where the new subtype declares a matching name.
+ public abstract class EnemyBase : MonoBehaviour
+ {
+ // The struct itself is empty; it only carries a PropertyDrawer.
+ // Place one field per root class — typically at the top of the Inspector.
+ [SerializeField] private ComponentTypeSelector _enemyType;
+
+ [SerializeField] [Min(0)] private float _health = 100f;
+
+ protected float Health => _health;
+
+ public abstract void Attack();
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs.meta
new file mode 100644
index 00000000..58777499
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/EnemyBase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 171cf8180e114d27bfbbd9e73261652f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs
new file mode 100644
index 00000000..1080395f
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs
@@ -0,0 +1,13 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class FastEnemy : EnemyBase
+ {
+ [SerializeField] [Min(0)] private float _speed = 25f;
+
+ public override void Attack() =>
+ Debug.Log("Fast enemy strikes!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs.meta
new file mode 100644
index 00000000..bc20a6fc
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/FastEnemy.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0667ffc8ad8b4dbba538545a568350f3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI.meta
new file mode 100644
index 00000000..d8c9d0e8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2b5e9a17b3ed13840b378e2bcd037e43
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs
new file mode 100644
index 00000000..b11c5598
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs
@@ -0,0 +1,27 @@
+using UnityEngine;
+using Aspid.FastTools.Types;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ // Demonstrates the IMGUI rendering path for ComponentTypeSelector.
+ //
+ // The mechanic mirrors the EnemyBase / Fast/Tank Enemy hierarchy: a single
+ // serialized ComponentTypeSelector field surfaces a subtype dropdown in the Inspector,
+ // and selecting a subtype rewrites m_Script in place — fields with matching names persist.
+ //
+ // The companion IMGUIEnemyBaseEditor (with editorForChildClasses: true) overrides
+ // OnInspectorGUI without CreateInspectorGUI, so every subclass renders through IMGUI
+ // and the picker goes through ComponentTypeSelectorPropertyDrawer.OnGUI instead of
+ // CreatePropertyGUI.
+ public abstract class IMGUIEnemyBase : MonoBehaviour
+ {
+ [SerializeField] private ComponentTypeSelector _enemyType;
+
+ [SerializeField] [Min(0)] private float _health = 100f;
+
+ protected float Health => _health;
+
+ public abstract void Attack();
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs.meta
new file mode 100644
index 00000000..d77aeda2
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIEnemyBase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5a3b9c7e1d2f48a6b95c3e1d27a8b604
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs
new file mode 100644
index 00000000..f0bd5fda
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs
@@ -0,0 +1,13 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class IMGUIFastEnemy : IMGUIEnemyBase
+ {
+ [SerializeField] [Min(0)] private float _speed = 25f;
+
+ public override void Attack() =>
+ Debug.Log("Fast IMGUI enemy strikes!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs.meta
new file mode 100644
index 00000000..6fc9c2e1
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUIFastEnemy.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2c8e91a4b73f4d62a85e9f3d7c41b80a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs
new file mode 100644
index 00000000..d8ea7938
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs
@@ -0,0 +1,13 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class IMGUITankEnemy : IMGUIEnemyBase
+ {
+ [SerializeField] [Min(0)] private float _armor = 50f;
+
+ public override void Attack() =>
+ Debug.Log("IMGUI tank attacks!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs.meta
new file mode 100644
index 00000000..a118ddb7
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/IMGUI/IMGUITankEnemy.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4c5b3e2d1a987456b8e063c41d27a8b6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs
new file mode 100644
index 00000000..55d9369b
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs
@@ -0,0 +1,13 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class TankEnemy : EnemyBase
+ {
+ [SerializeField] [Min(0)] private float _armor = 50f;
+
+ public override void Attack() =>
+ Debug.Log("Tank attacks!");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs.meta
new file mode 100644
index 00000000..c43eeee8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Enemies/TankEnemy.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1fbf86c93e134055adf1f2a443bd502c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers.meta
new file mode 100644
index 00000000..76fee5e4
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d74d3a5c5ac44c18858299d4cc480df1
+timeCreated: 1776546333
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs
new file mode 100644
index 00000000..83552853
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs
@@ -0,0 +1,8 @@
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public abstract class AbilityModifier
+ {
+ public abstract void Apply();
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs.meta
new file mode 100644
index 00000000..bab24a37
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/AbilityModifier.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 96e6f60018cc4efab26e8543e24da49b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs
new file mode 100644
index 00000000..71e1cdcb
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class CooldownReductionModifier : AbilityModifier
+ {
+ public override void Apply() =>
+ Debug.Log($"{nameof(CooldownReductionModifier)} applied.");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs.meta
new file mode 100644
index 00000000..2da45e21
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/CooldownReductionModifier.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4f3b09571b8549df90b7841e94f07410
+timeCreated: 1776546350
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs
new file mode 100644
index 00000000..b57e900a
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class DoubleDamageModifier : AbilityModifier
+ {
+ public override void Apply() =>
+ Debug.Log($"{nameof(DoubleDamageModifier)} applied.");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs.meta
new file mode 100644
index 00000000..20f076ef
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/DoubleDamageModifier.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 44b4b7f2062c4b138266c17847aa2b02
+timeCreated: 1776546354
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs
new file mode 100644
index 00000000..f83907e4
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.Types
+{
+ public sealed class RangeBoostModifier : AbilityModifier
+ {
+ public override void Apply() =>
+ Debug.Log($"{nameof(RangeBoostModifier)} applied.");
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs.meta
new file mode 100644
index 00000000..93b4b780
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/Types/Scripts/Modifiers/RangeBoostModifier.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a6d9ae55ba3148508c5c58f03ac9b54e
+timeCreated: 1776546345
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements.meta
new file mode 100644
index 00000000..efd874de
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: dd811fb9dcc9c49898350cf0a1f5657e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md
new file mode 100644
index 00000000..989c2829
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md
@@ -0,0 +1,16 @@
+# VisualElements Sample
+
+A custom UIToolkit Inspector for an `AbilityConfig` ScriptableObject that showcases the fluent `VisualElement` extension API — building a card-style inspector entirely in code, with a status badge that reacts to Mana Cost edits.
+
+Look at:
+
+- `Scripts/Editor/AbilityConfigEditor.cs:23` — `CreateInspectorGUI` composes the card from plain `VisualElement` / `Label` / `HelpBox` chained with the Aspid fluent extensions (`.SetPadding`, `.SetBorderRadius`, `.SetBorderColor`, `.SetFlexDirection`, `.AddChild`).
+- `Scripts/Editor/AbilityConfigEditor.cs:38` — `target.GetScriptName()` (from `Aspid.FastTools.Editors`) is used as the header title.
+- `Scripts/Editor/AbilityConfigEditor.cs:65` — `PropertyField(...).AddValueChanged(_ => UpdateState())` re-runs the badge / help-box logic on every Mana Cost edit.
+- `Scripts/Editor/AbilityConfigEditor.cs:88` — `UpdateState()` flips the badge text/color and toggles `helpBox.SetDisplay(...)` whenever `ManaCost is 0`.
+
+To try it:
+
+1. Select `ScriptableObjects/fireball_1.asset` (paid) or `ScriptableObjects/fireball_free.asset` in the Project window — the custom inspector appears in the Inspector panel.
+2. Edit the fields. Set `Mana Cost` to `0` to see the status badge switch to "FREE" and the warning help box appear inline.
+3. Or create your own via `Assets > Create > Aspid > FastTools > Samples > Ability Config`.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md.meta
new file mode 100644
index 00000000..5efd5561
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4e6850127fdae442c85dc56d37cd070f
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md
new file mode 100644
index 00000000..13a947cb
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md
@@ -0,0 +1,16 @@
+# Пример VisualElements
+
+Кастомный UIToolkit Inspector для `ScriptableObject` `AbilityConfig`, демонстрирующий fluent-API расширений `VisualElement` — карточный inspector, собранный целиком в коде, со статус-бейджем, реагирующим на правки Mana Cost.
+
+Смотрите:
+
+- `Scripts/Editor/AbilityConfigEditor.cs:23` — `CreateInspectorGUI` собирает карточку из обычных `VisualElement` / `Label` / `HelpBox` с цепочкой Aspid fluent-расширений (`.SetPadding`, `.SetBorderRadius`, `.SetBorderColor`, `.SetFlexDirection`, `.AddChild`).
+- `Scripts/Editor/AbilityConfigEditor.cs:38` — `target.GetScriptName()` (из `Aspid.FastTools.Editors`) используется как заголовок шапки.
+- `Scripts/Editor/AbilityConfigEditor.cs:65` — `PropertyField(...).AddValueChanged(_ => UpdateState())` перезапускает логику бейджа и help-box при каждой правке Mana Cost.
+- `Scripts/Editor/AbilityConfigEditor.cs:88` — `UpdateState()` переключает текст и цвет бейджа и `helpBox.SetDisplay(...)`, когда `ManaCost is 0`.
+
+Чтобы попробовать:
+
+1. Выберите `ScriptableObjects/fireball_1.asset` (платная) или `ScriptableObjects/fireball_free.asset` в окне Project — кастомный inspector появится в панели Inspector.
+2. Отредактируйте поля. Установите `Mana Cost` в `0`, чтобы увидеть, как статус-бейдж переключится на "FREE", и появится встроенный help box с предупреждением.
+3. Или создайте свой через `Assets > Create > Aspid > FastTools > Samples > Ability Config`.
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md.meta
new file mode 100644
index 00000000..6807ac1e
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/README_RU.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: be7f508192a3b4c5d6e7f80910a1b2c3
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects.meta
new file mode 100644
index 00000000..e8422d2c
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3c3b500b54c6c4141b78263db88a607d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset
new file mode 100644
index 00000000..6c086f81
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6, type: 3}
+ m_Name: fireball_1
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.VisualElements::Aspid.FastTools.Samples.VisualElements.AbilityConfig
+ _abilityName: Fireball
+ _description: Hurls a ball of fire that explodes on impact.
+ _manaCost: 100
+ _cooldown: 3.5
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset.meta
new file mode 100644
index 00000000..7ffe7e53
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_1.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47df49f0b76bc4bd38c447548a7dfbee
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset
new file mode 100644
index 00000000..1d6e18f8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6, type: 3}
+ m_Name: fireball_free
+ m_EditorClassIdentifier: Aspid.FastTools.Samples.VisualElements::Aspid.FastTools.Samples.VisualElements.AbilityConfig
+ _abilityName: Fireball - Free
+ _description: Hurls a ball of fire that explodes on impact.
+ _manaCost: 0
+ _cooldown: 50
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset.meta
new file mode 100644
index 00000000..16cf7d96
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/ScriptableObjects/fireball_free.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts.meta
new file mode 100644
index 00000000..3eb3994f
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 48284416efb4f482fb664e2ff9dc4f22
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs
new file mode 100644
index 00000000..50d5bfe4
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs
@@ -0,0 +1,23 @@
+using UnityEngine;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.VisualElements
+{
+ [CreateAssetMenu(fileName = "New Ability Config", menuName = "Aspid/FastTools/Samples/Ability Config")]
+ public sealed class AbilityConfig : ScriptableObject
+ {
+ [SerializeField] private string _abilityName = "New Ability";
+ [SerializeField] [TextArea] private string _description;
+
+ [SerializeField] [Min(0)] private int _manaCost = 10;
+ [SerializeField] [Min(0f)] private float _cooldown = 1f;
+
+ public int ManaCost => _manaCost;
+
+ public float Cooldown => _cooldown;
+
+ public string AbilityName => _abilityName;
+
+ public string Description => _description;
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs.meta
new file mode 100644
index 00000000..bef311eb
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/AbilityConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef
new file mode 100644
index 00000000..0f914910
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "Aspid.FastTools.Samples.VisualElements",
+ "rootNamespace": "",
+ "references": [
+ "Aspid.FastTools.Unity"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef.meta
new file mode 100644
index 00000000..14109c07
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Aspid.FastTools.Samples.VisualElements.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6a3c7b14e2f94d71b15d0e8f2f4a1c9d
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor.meta
new file mode 100644
index 00000000..f75b2e15
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e59e68d2d7da84e24b1c6be44953f34b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs
new file mode 100644
index 00000000..e844e623
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs
@@ -0,0 +1,103 @@
+using UnityEditor;
+using UnityEngine;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+using Aspid.FastTools.Editors;
+using Aspid.FastTools.UIElements;
+using Aspid.FastTools.UIElements.Editors;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Samples.VisualElements.Editors
+{
+ [CustomEditor(typeof(AbilityConfig))]
+ internal sealed class AbilityConfigEditor : Editor
+ {
+ private static readonly Color _cardBackground = new(0.16f, 0.17f, 0.19f);
+ private static readonly Color _cardBorder = new(0.26f, 0.28f, 0.31f);
+ private static readonly Color _dividerColor = new(0.22f, 0.24f, 0.27f);
+ private static readonly Color _titleColor = new(0.93f, 0.93f, 0.94f);
+ private static readonly Color _subtleColor = new(0.58f, 0.61f, 0.66f);
+ private static readonly Color _accent = new(0.42f, 0.69f, 1.00f);
+ private static readonly Color _warning = new(1.00f, 0.76f, 0.30f);
+
+ public override VisualElement CreateInspectorGUI()
+ {
+ var config = (AbilityConfig)target;
+
+ var statusBadge = new Label()
+ .SetFontSize(10)
+ .SetUnityFontStyleAndWeight(FontStyle.Bold)
+ .SetUnityTextAlign(TextAnchor.MiddleCenter)
+ .SetLetterSpacing(1)
+ .SetPadding(top: 3, bottom: 3, left: 10, right: 10)
+ .SetBorderRadius(10)
+ .SetBorderWidth(1);
+
+ var titles = new VisualElement()
+ .SetFlexGrow(1)
+ .AddChild(new Label(target.GetScriptName())
+ .SetColor(_titleColor)
+ .SetFontSize(15)
+ .SetUnityFontStyleAndWeight(FontStyle.Bold))
+ .AddChild(new Label("ABILITY CONFIGURATION")
+ .SetColor(_subtleColor)
+ .SetFontSize(10)
+ .SetLetterSpacing(2)
+ .SetMarginTop(3));
+
+ var header = new VisualElement()
+ .SetFlexDirection(FlexDirection.Row)
+ .SetAlignItems(Align.Center)
+ .SetPadding(top: 12, bottom: 12, left: 14, right: 14)
+ .AddChild(titles)
+ .AddChild(statusBadge);
+
+ var divider = new VisualElement()
+ .SetHeight(1)
+ .SetBackgroundColor(_dividerColor);
+
+ var helpBox = new HelpBox()
+ .SetText("This ability costs no mana — is that intentional?")
+ .SetMessageType(HelpBoxMessageType.Warning)
+ .SetMarginTop(8)
+ .SetBorderRadius(6);
+
+ var manaCostField = new PropertyField(serializedObject.FindProperty("_manaCost"))
+ .AddValueChanged(_ => UpdateState());
+
+ var body = new VisualElement()
+ .SetPadding(top: 12, bottom: 12, left: 14, right: 14)
+ .AddChild(new PropertyField(serializedObject.FindProperty("_abilityName")))
+ .AddChild(new PropertyField(serializedObject.FindProperty("_description")))
+ .AddChild(new PropertyField(serializedObject.FindProperty("_cooldown")))
+ .AddChild(manaCostField)
+ .AddChild(helpBox);
+
+ var card = new VisualElement()
+ .SetBackgroundColor(_cardBackground)
+ .SetBorderColor(_cardBorder)
+ .SetBorderWidth(1)
+ .SetBorderRadius(10)
+ .AddChild(header)
+ .AddChild(divider)
+ .AddChild(body);
+
+ UpdateState();
+ return card;
+
+ void UpdateState()
+ {
+ var manaCost = config.ManaCost;
+ var isFree = manaCost is 0;
+ var color = isFree ? _warning : _accent;
+
+ statusBadge
+ .SetText(isFree ? "FREE" : $"{manaCost} MP")
+ .SetColor(color)
+ .SetBorderColor(color);
+
+ helpBox.SetDisplay(isFree ? DisplayStyle.Flex : DisplayStyle.None);
+ }
+ }
+ }
+}
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs.meta
new file mode 100644
index 00000000..26a6038c
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/AbilityConfigEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scripts/Editor/Aspid.UnityFastTools.VisualElements.Editor.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef
similarity index 72%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scripts/Editor/Aspid.UnityFastTools.VisualElements.Editor.asmdef
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef
index ad6ee987..7c4ce859 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/VisualElements/Scripts/Editor/Aspid.UnityFastTools.VisualElements.Editor.asmdef
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef
@@ -1,10 +1,10 @@
{
- "name": "Aspid.UnityFastTools.VisualElements.Editor",
- "rootNamespace": "",
+ "name": "Aspid.FastTools.Samples.VisualElements.Editor",
"references": [
+ "GUID:d8b63aba1907145bea998dd612889d6b",
"GUID:7c010b89992542508a6b6189977e64d4",
"GUID:94dcbbdbbd3ca48b891ee4fc8455c434",
- "GUID:0acbb74fa21d2442393a327f6c8d5639"
+ "GUID:6a3c7b14e2f94d71b15d0e8f2f4a1c9d"
],
"includePlatforms": [
"Editor"
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef.meta
new file mode 100644
index 00000000..1d07f6cd
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Samples~/VisualElements/Scripts/Editor/Aspid.FastTools.Samples.VisualElements.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8b2f4c9e7a1d4862a97b5f3e1c2d8e7a
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source.meta
new file mode 100644
index 00000000..cd7ea7a8
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c1865534b470439a979710a6ee77f7dc
+timeCreated: 1772957434
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef
new file mode 100644
index 00000000..d15c03b6
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef
@@ -0,0 +1,3 @@
+{
+ "name": "Aspid.FastTools"
+}
diff --git a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef.meta
similarity index 76%
rename from Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef.meta
rename to Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef.meta
index 379034da..ea49f73a 100644
--- a/Aspid.UnityFastTools/Assets/Plugins/Aspid/UnityFastTools/Samples/ProfilerMarkers/Scripts/Aspid.UnityFastTools.ProfilerMarkers.asmdef.meta
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Aspid.FastTools.asmdef.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 72fb96818833940038502bdd77f5dff9
+guid: c8f809693df904cca816d5d7a67dff48
AssemblyDefinitionImporter:
externalObjects: {}
userData:
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions.meta b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions.meta
new file mode 100644
index 00000000..64fdaf8a
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b03005925b304e1a90f79d4f1bda93bf
+timeCreated: 1772957451
\ No newline at end of file
diff --git a/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions/TypeExtensions.cs b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions/TypeExtensions.cs
new file mode 100644
index 00000000..30773f3d
--- /dev/null
+++ b/Aspid.FastTools/Assets/Plugins/Aspid/FastTools/Source/Extensions/TypeExtensions.cs
@@ -0,0 +1,45 @@
+#nullable enable
+using System;
+using System.Reflection;
+using System.Collections.Generic;
+
+// ReSharper disable once CheckNamespace
+namespace Aspid.FastTools.Reflection
+{
+ public static class TypeExtensions
+ {
+ ///
+ /// Returns the members of