From c03afe8e332531fb964266910b6e3e8e5bf44701 Mon Sep 17 00:00:00 2001 From: elham-saboori Date: Tue, 29 Sep 2026 17:22:50 +0000 Subject: [PATCH] chore: publish 12 skills (asset-transformer-toolkit, audio-setup-mixers, build-gtk, +9) --- skills/asset-transformer-toolkit/SECURITY.md | 4 - skills/audio-setup-mixers/SECURITY.md | 4 - skills/build-gtk/SKILL.md | 247 +++++++++++++ skills/build-gtk/references/graph-api.md | 324 ++++++++++++++++++ skills/build-gtk/references/pitfalls.md | 86 +++++ .../references/runtime-and-visualization.md | 196 +++++++++++ .../build-gtk/references/state-machine-api.md | 286 ++++++++++++++++ .../generate-editor-search-query/SECURITY.md | 4 - skills/implement-in-app-purchases/README.md | 233 ------------- skills/implement-in-app-purchases/SKILL.md | 2 +- .../references/path-implement-iap-d2c.md | 22 +- skills/migrate-birp-to-urp/SECURITY.md | 4 - skills/optimize-audio/SECURITY.md | 4 - skills/optimize-web/SECURITY.md | 4 - skills/sprite-editor/SECURITY.md | 4 - skills/ui-ugui/SKILL.md | 2 +- skills/ui-uitk/SKILL.md | 14 +- skills/urp-postprocessing/SECURITY.md | 4 - 18 files changed, 1152 insertions(+), 292 deletions(-) create mode 100644 skills/build-gtk/SKILL.md create mode 100644 skills/build-gtk/references/graph-api.md create mode 100644 skills/build-gtk/references/pitfalls.md create mode 100644 skills/build-gtk/references/runtime-and-visualization.md create mode 100644 skills/build-gtk/references/state-machine-api.md delete mode 100644 skills/implement-in-app-purchases/README.md diff --git a/skills/asset-transformer-toolkit/SECURITY.md b/skills/asset-transformer-toolkit/SECURITY.md index d78d417..74dc7c7 100644 --- a/skills/asset-transformer-toolkit/SECURITY.md +++ b/skills/asset-transformer-toolkit/SECURITY.md @@ -13,7 +13,3 @@ This skill has the agent write C# and run it inside the user's live Unity Editor - **Only the user's own Editor.** The code runs in the Editor open on this machine. It can't reach another machine or another user's Editor. - **Narrow purpose.** The scripts create or modify RuleSet assets through the documented Asset Transformer API, then validate them against the checklist in `references/`. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - -## Background - -This skill ran code through the AI Assistant's built-in `RunCommand` tool before it moved to the public catalog. Automated checks don't flag it, because it never names `unity command eval`, so this file records the capability explicitly. Ziyi Zhang accepted this risk on 2026-09-25. diff --git a/skills/audio-setup-mixers/SECURITY.md b/skills/audio-setup-mixers/SECURITY.md index 06dff75..e2a4252 100644 --- a/skills/audio-setup-mixers/SECURITY.md +++ b/skills/audio-setup-mixers/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25. diff --git a/skills/build-gtk/SKILL.md b/skills/build-gtk/SKILL.md new file mode 100644 index 0000000..b8e5904 --- /dev/null +++ b/skills/build-gtk/SKILL.md @@ -0,0 +1,247 @@ +--- +name: build-gtk +description: >- + Builds node-based Editor tools on Unity Graph Toolkit (GTK) for Unity 6.6 and newer, using the + public Graph API for graphs, nodes, ports, subgraphs and validation, and the State Machine API on + Unity 6.7 and newer for states, transitions and conditions. It also compiles authored graphs into + runtime assets with a scripted importer and adds live debug views, toolbar buttons and context + menus. Make sure to use this skill whenever the user mentions Graph Toolkit, GTK, Graph Tools + Foundation (GTF) or Unity.GraphToolkit.Editor, asks for a custom graph editor, node editor or state + machine editor tool, wants to migrate a GraphView or UnityEditor.Experimental.GraphView tool, or only + says "node graph", "visual editor", "dialogue graph" or "behaviour graph tool". Not for Shader Graph, + VFX Graph, Animator Controller state machines, or using the Unity Behavior package as an end user. +required_editor_version: ">=6000.6" +--- + +# Unity Graph Toolkit (GTK) + +Build Editor graph tools and state machine tools on Unity's Graph Toolkit module, using only its +public API in the `Unity.GraphToolkit.Editor` namespace. + +## Important + +- Create a restore point you can roll back to if your changes fail. +- Do only what's asked. Don't change unrelated assets or files. Avoid long explanations. +- Before you go forward with a workflow or diagnosis, open and read the reference file(s) named in + that workflow and the [Topic map](#topic-map) page(s) that cover the thing you intend to do. Name + the file(s) you read in your response. This is because your training knowledge about Unity might be + out of date, incorrect, or for the wrong Unity version, and Graph Toolkit changed between 6000.4, + 6000.6 and 6000.7. + +## References + +Read these as needed. Paths are relative to this skill's folder. + +- `references/graph-api.md` — the Graph API surface: Graph, Node, port and option builders, context + and block nodes, subgraphs, variables, validation, toolbar and context menus. Read before writing any + graph-tool code. +- `references/state-machine-api.md` — the State Machine API (Unity 6.7+): StateMachine, State, + Condition, transitions and rules, self transitions, custom views. Read whenever states, transitions + or conditions are involved. +- `references/runtime-and-visualization.md` — compiling a graph or state machine into a runtime asset + with a ScriptedImporter, and showing live execution in the graph editor. Read when the user wants the + authored data used in the game or debugged at runtime. +- `references/pitfalls.md` — the mistakes that survive compilation. Read before handing off, and + whenever the user reports a compile error, an empty Add menu, a missing node, or an exception. + +## Step 1: Check the Editor version and pick the API + +Read `m_EditorVersion` in `ProjectSettings/ProjectVersion.txt` before writing code. Graph Toolkit is +an Editor module, so there is no package to install. `com.unity.graphtoolkit` is a deprecated shim +and must not be added to the manifest. + +| Editor version | What is available | +|---|---| +| Older than 6000.4 | Only the retired experimental package (0.x). Its API differs from this skill. Recommend upgrading. | +| 6000.4 to 6000.5 | Module present, but without port capacity, type casting, wire and node visualization, USS node styling, or element IDs. | +| 6000.6 | The Graph API this skill documents. | +| 6000.7 and newer | Everything above plus the State Machine API, `NodeView` / `StateView` / `ConditionView` custom UI, `[GraphMenu]` / `[BlackboardMenu]` context menus, the `GraphLogger.GraphChanges` change delta, port type-following, and Blackboard type restriction. Members marked **6.7** in the references. | + +If the user asks for a state machine tool on an Editor older than 6000.7, say plainly that the +State Machine API is not available there. Do not emulate it with the Graph API unless asked. + +## Mental model + +- Graph Toolkit is an authoring framework only. It draws and persists graphs; it never executes them. + The user writes the runtime data model and the code that runs it, usually fed by a ScriptedImporter. +- One asset type per `Graph` or `StateMachine` subclass, identified by the file extension passed to + `[Graph("ext")]` or `[StateMachine("ext")]`. +- Discovery is by reflection. Node, state and condition classes in the same assembly as the graph + class are listed automatically. Classes in other assemblies opt in with `[UseWithGraph]` or + `[UseWithStateMachine]`. Abstract classes are never listed. +- Every authoring class is `[Serializable]` and lives in an Editor-only assembly, either an `Editor` + folder or an asmdef restricted to the Editor platform. +- Only the public API compiles for users. If a name from the left column appears in a draft, stop and + use the right column: + + | Never write | Write instead | + |---|---| + | `GraphModel`, `GraphObject`, `GraphTool`, `Stencil`, `BaseGraphTool` | `Graph` and `GraphDatabase` | + | `NodeModel`, `PortModel`, `WireModel` | `Node`, `IPort`, `Wire` | + | `StateModel`, `TransitionModel`, `ConditionModel` | `State`, `ITransition`, `Condition` | + | `GraphViewEditorWindow`, `GraphElementsExtensionMethodsCache`, `ModelView` | the window is provided; `NodeView` on 6000.7 for custom UI | + | `UnityEditor.Experimental.GraphView`, `graphViewChanged`, `Port.Create` | no equivalent; model the tool as `Graph` and `Node` classes | + | `com.unity.graphtoolkit` in the manifest | nothing; the module is built in | + +## Workflow A: build a graph tool + +Read `references/graph-api.md` now, before writing code; member names and builder chains come from +there, not from memory. Read `references/runtime-and-visualization.md` as well when step 5 applies. + +1. **Graph class.** `[Graph(AssetExtension)] [Serializable] class MyGraph : Graph` with a + `public const string AssetExtension`. Add a `[MenuItem("Assets/Create/...")]` static method that + calls `GraphDatabase.PromptInProjectBrowserToCreateNewAsset()`. Pass + `GraphOptions.SupportsSubgraphs` in the attribute when subgraphs are wanted. +2. **Node classes.** `[Serializable] class MyNode : Node`. Override `OnDefinePorts` and end every port + builder chain with `.Build()`. Override `OnDefineOptions` for inspector-editable settings, and read + them inside `OnDefinePorts` with `GetNodeOptionByName(name).TryGetValue(out var v)` when ports + depend on them. Call `.Delayed()` on count-like options so ports are not rebuilt on every keystroke. + Use `[Node("Category/Path", iconPath, title, stylesheet)]` for placement, icon, default title and USS. +3. **Validation.** Override `OnGraphChanged(GraphLogger logger)` and report with + `logger.LogError`, `LogWarning` or `Log`, passing the offending node or port as context so the + marker appears on it. Never mutate the graph in this callback; put the fix in a `GraphLogAction` + attached to the message, a menu item, or the importer. +4. **Structure, as requested.** Context and block nodes, subgraphs, blackboard variables, type casting + through `IsConnectionAllowed`, a toolbar element, or a context menu. Details in `graph-api.md`. +5. **Output.** A `ScriptedImporter` registered on the same extension compiles the graph into a plain + runtime asset. The shape is always the same: + + ```csharp + var graph = GraphDatabase.LoadGraphForImporter(ctx.assetPath); // never LoadGraph here + if (graph == null) return; // bad path or type: log and stop + var start = graph.GetNodes().OfType().FirstOrDefault(); + if (start == null) return; // OnGraphChanged already reported it + var next = start.GetOutputPortByName("Next").FirstConnectedPort?.GetNode(); // null when unconnected + var titlePort = next.GetInputPortByName("Title"); + titlePort.TryGetValue(out var title); // the value typed on the node + if (titlePort.IsConnected) { /* resolve titlePort.FirstConnectedPort.GetNode() upstream instead */ } + ctx.AddObjectToAsset("Runtime", runtime); ctx.SetMainObject(runtime); + ``` + + The runtime asset and its assembly must not reference `Unity.GraphToolkit.Editor`; keep `Hash128` + IDs as `Hash128`. Debug views and code-built graphs are in `runtime-and-visualization.md`. +6. **Verify** as described below. + +## Workflow B: build a state machine tool (Unity 6.7 and newer) + +Read `references/state-machine-api.md` now; there is no manual walkthrough for this API yet, so that +file and the Script Reference are the only accurate sources for its member names. + +1. **State machine class.** `[StateMachine(AssetExtension)] [Serializable] class MySM : StateMachine` + plus a menu item calling `StateMachineDatabase.PromptInProjectBrowserToCreateNewAsset()`. +2. **States.** `[Serializable] class Patrol : State`. States have no ports; the state machine owns the + transitions. Options declared in `OnDefineOptions` appear only in the Graph Inspector. +3. **Conditions.** `[Serializable] [Condition("Health")] class HealthCondition : Condition` with + `protected override bool DisplayComparisonDropdown => true` when a comparison operator is wanted. + Derive from `Condition` directly for a valueless trigger. Group (And/Or) and variable conditions are + built in; do not reimplement them. +4. **Optional.** Custom self transitions (`SelfTransition` + `[Transition(...)]`), custom UI through + `StateView` and `ConditionView`, and `[StateMachineMenu]` / `[ConditionMenu]` entries. +5. **Validation.** Override `OnStateMachineChanged(StateMachineLogger logger)`. Same rules as + `OnGraphChanged`. +6. **Output.** A `ScriptedImporter` loads with `StateMachineDatabase.LoadStateMachineForImporter`, + then walks `GetStates()`, each state's `GetOutgoingTransitions()`, each transition's `GetRules()`, + and each rule's `RootCondition` tree. That tree always contains the built-in kinds as well as the + user's classes, so the compiler must handle `IGroupCondition` (recurse, honour `Operation`) and + `IVariableCondition` (`Variable.Name`, `Comparison`, `Value`) before matching custom `Condition` + types. + +## Editing a graph from code + +When a menu item, generator or tool builds or edits an asset, bracket the mutations: +`LoadGraph`/`CreateGraph` → `UndoBeginRecordGraph("Action")` → `AddNode`, `Connect`, +`port.TrySetValue`, `CreateVariable` → `GraphDatabase.SaveGraph(graph)` → `UndoEndRecordGraph()`. +The state machine equivalents are `UndoBeginRecordStateMachine`, `Connect(fromState, toState)`, +`SaveStateMachine` and `UndoEndRecordStateMachine`. Mutating inside `OnEnable`, `OnDisable`, +`OnGraphChanged` or `OnStateMachineChanged` throws `InvalidOperationException`. + +## Decisions + +| The user wants | Do this | Why | +|---|---|---| +| Limit how many wires a port accepts | `.WithCapacity(PortCapacity.Single)` (or `Multi`, `None`) in the port builder | Ports default to multiple wires, so the framework enforces the limit only if you declare it | +| Connect an `int` output to a `float` input | Override `Graph.IsConnectionAllowed(IPort output, IPort input)` | Ports of different types refuse to connect by default; the graph only records the wire, your importer converts | +| One port that accepts several types, like Shader Graph | 6000.7+: `.WithDataTypes(typeof(float), typeof(int), typeof(Vector3))` on the builder. 6000.6: an untyped port plus `IsConnectionAllowed` | `WithDataTypes` does not exist on 6000.6 | +| An execution-flow port with no data type | `context.AddInputPort("In")` with no type; its `DataType` is `Untyped`. Add `.WithConnectorUI(PortConnectorUI.Arrowhead)` for a flow look | Flow ports carry no value; typing them invites wrong connections | +| Hide a node type from the Add menu | Make it `abstract`, or move it to another assembly without `[UseWithGraph]`, or set `GraphOptions.DisableAutoInclusionOfNodesFromGraphAssembly` and opt nodes in explicitly | Every concrete node class in the graph's assembly is listed automatically | +| Group nodes in the Add menu | `[Node("Category/Sub")]`. The class or title name is appended after the path | The attribute's first argument is a folder path, not the node's title | +| Know what changed, like the old `graphViewChanged` | 6000.7+: `OnGraphChanged` and `logger.GraphChanges.ChangedNodes` with `ChangeKinds` flags for added and modified nodes. Removed nodes on any version, and everything on 6000.6: diff a cached `HashSet` of node IDs against `GetNodes()` | `GraphChanges` was added in 6000.7 and never reports removed nodes, only removed ports | +| Several kinds of subgraph | `GraphOptions.SupportsSubgraphs` on the main graph, `[Subgraph(typeof(MainGraph))]` on each subgraph `Graph` class | Without the attribute the main graph type doubles as the only subgraph type | +| Ports on a subgraph node | Blackboard variables of kind `VariableKind.Input` or `Output` inside the subgraph | Subgraph nodes have no `OnDefinePorts`; their ports mirror the subgraph's variables | +| Node icon or USS look | `[Node(category, iconPath, title, stylesheet)]`; `d_` prefix for the dark-theme icon file | The Editor picks the `d_` file in the dark theme and falls back otherwise | +| Show which node runs and what flows through ports at runtime | GraphVisualization API, see `runtime-and-visualization.md` | Graph Toolkit has no runtime; the game must push state back into the open window | +| An Animator-like state machine editor | Workflow B on 6000.7+. On 6000.6, explain it is not available | The State Machine API does not exist before 6000.7 | +| Extra UI inside a node | `NodeView` on 6000.7+. On 6000.6, only USS, `DefaultColor` and `FillAmount` | `NodeView` was added in 6000.7 | + +## Constraints + +- Use member names exactly as the references spell them: properties are PascalCase + (`FirstConnectedPort`, `IsConnected`), attributes take positional constructor arguments, and IDs are + `Hash128`. When a member is not listed in the references, look it up at the URL pattern below rather + than guessing its name or casing, because a guessed member fails to compile and the user cannot tell + a typo from a missing feature. +- Link to the Script Reference or manual instead of restating them, so the answer stays correct when + the docs change. +- Keep runtime assemblies free of `Unity.GraphToolkit.Editor`, and put visualization code inside a + runtime assembly under `#if UNITY_EDITOR`, because the module is Editor-only and any reference to it + breaks the player build. +- Keep the user's existing tool structure and do not restyle or reorganize nodes that were not + mentioned, because node and port names are lookup keys that importers and saved assets depend on. +- Do not use Graph Toolkit internals or private types as a workaround, because they are not accessible + from user code and change without notice. If the public API cannot do something, say so and point at + the community thread list below. + +## Verify + +0. Read `references/pitfalls.md` and check the code against it before showing it to the user. +1. The project compiles with no errors. If the Unity CLI is available, use it to build or run the + Editor headless; otherwise ask the user to focus the Editor and report the Console. +2. Create an asset from the new menu item, double-click it, add every node or state type from the Add + menu, connect them, save, close and reopen. The Console must show no errors or warnings. +3. With an importer, select the asset and confirm the produced runtime object is the main asset in + the Inspector. +3b. When you wrote runtime code (an executor, a debug view), add `Debug.Log` lines that prove the + behaviour, such as the state entered or the node executed, enter Play mode, and read them. Remove or + guard them once the behaviour is confirmed. +4. Re-read `references/pitfalls.md` and fix anything it flags. +5. If a step fails or the Console reports an error, go back and reread the reference file and the + [Topic map](#topic-map) page for that step before retrying; the fix is usually a member name or a + version gate you missed. + +## Final report + +Give the user a short checklist: the files you created or changed and where they go, how to create +and open the first asset, what the importer produces, and which Editor version the code targets. List +what is left for them to decide or do next, such as more node types, the runtime executor, or the debug +view, and any Console error you could not verify because no Editor was available. + +## Topic map + +Prefer `WebFetch` over `WebSearch`; it is faster and lands on the exact page. Only fetch what you need. +Replace `` before fetching: + +- `docs.unity.com/en-us/engine/` and `docs.unity3d.com//Documentation`: the project's + Editor version from `ProjectSettings/ProjectVersion.txt`, for example `6000.6` or `6000.7`. +- `docs.unity3d.com/Packages/com.unity.graphtoolkit-samples@`: the samples package + version shown in the Package Manager, for example `0.6`. + +Pages, all under `https://docs.unity.com/en-us/engine//`, served as markdown when the `.md` +suffix is kept: + +- Manual index: `manual/extending-the-editor/gtk-index.md` +- Implementing a graph tool: `manual/extending-the-editor/gtk-index/implementing-a-graph-tool.md`, with + child pages `implement-a-graph-tool.md`, `implement-nodes.md`, `implement-node-options.md`, + `implement-context-nodes.md`, `implement-block-nodes.md`, `type-cast-ports.md`, + `add-custom-toolbar-actions.md`, `add-subgraph-support.md`, `graph-processing.md` under that folder +- Graph window and panels: `manual/extending-the-editor/gtk-index/landing-graph-interface.md`, with + `graph-window.md`, `blackboard.md`, `graph-inspector.md`, `minimap.md` under it +- Script Reference: `script-reference/unity/graphtoolkit/editor/.md` and + `...//.md`, all lowercase, generic arity dropped (`condition.md` for `Condition`). + Example: `script-reference/unity/graphtoolkit/editor/graph/ongraphchanged.md`. State machine types + exist from `6000.7`. The older form `https://docs.unity3d.com//Documentation/ScriptReference/Unity.GraphToolkit.Editor..html` + also resolves, as HTML. +- Samples: `https://docs.unity3d.com/Packages/com.unity.graphtoolkit-samples@/manual/index.html`. + Install `com.unity.graphtoolkit-samples` by name in the Package Manager, then import Texture Maker + (importer), Visual Novel Director (custom runtime and debug view) or Dungeon Graph Generator + (building a graph from code). +- Community: https://discussions.unity.com/tag/graph-toolkit diff --git a/skills/build-gtk/references/graph-api.md b/skills/build-gtk/references/graph-api.md new file mode 100644 index 0000000..05a0ac6 --- /dev/null +++ b/skills/build-gtk/references/graph-api.md @@ -0,0 +1,324 @@ +# Graph API reference (Unity 6.6 and newer) + +All types live in `Unity.GraphToolkit.Editor` unless stated. Members marked **6.7** exist only on +Unity 6000.7 and newer. Every signature below was taken from the module source; when something is +not listed here, look it up at +`https://docs.unity3d.com//Documentation/ScriptReference/Unity.GraphToolkit.Editor..html`, +with `` the project's Editor version, for example `6000.6`. + +## Contents + +1. Graph class and asset lifecycle +2. Nodes +3. Ports +4. Node options +5. Context and block nodes +6. Subgraphs +7. Variables and constants +8. Validation with OnGraphChanged +9. Type casting between ports +10. Toolbar elements +11. Context menus (6.7) +12. Node look: colors, accent, USS, custom UI +13. Wires + +## 1. Graph class and asset lifecycle + +```csharp +[Graph(AssetExtension)] // optional second arg: GraphOptions flags +[Serializable] +class MyGraph : Graph +{ + public const string AssetExtension = "mygraph"; + + [MenuItem("Assets/Create/My Tool/My Graph")] + static void CreateAssetFile() => GraphDatabase.PromptInProjectBrowserToCreateNewAsset(); +} +``` + +`GraphOptions` flags: `SupportsSubgraphs`, `DisableAutoInclusionOfNodesFromGraphAssembly`. +`Default` and `None` are 0. + +`GraphDatabase` (static): + +- `PromptInProjectBrowserToCreateNewAsset(string defaultName = "New Graph")` +- `T CreateGraph(string assetPath)`, `T LoadGraph(string assetPath)`, `SaveGraph(Graph)` +- `T LoadGraphForImporter(string assetPath)` — the only loader to use inside a `ScriptedImporter` +- `GUID GetGraphAssetGUID(Graph)`, `string GetGraphAssetPath(Graph)` + +`Graph` members: + +- `Name`, `NodeCount`, `VariableCount`, `Hash128 ID`, `GUID AssetGuid` +- `GetNodes()`, `GetNode(int)`, `AddNode(Node)`, `RemoveNode(INode)` +- `bool Connect(IPort output, IPort input)`, `bool Disconnect(IPort output, IPort input)`, + `Wire GetWire(IPort output, IPort input)` +- `CreateVariable(name, defaultValue, VariableKind)`, `CreateVariable(name, Type, object, VariableKind)`, + `GetVariables()`, `GetVariables(SortMethod)`, `GetVariable(int)`, `RemoveVariable(IVariable, bool forceRemove)` +- `CreateConstantNode(Vector2 position, T defaultValue)`, + `AddVariableNode(IVariable, Vector2 position)`, **6.7** `AddVariableNode(IVariable, Vector2, VariableNodeMode)`, + `AddSubgraphNode(Graph subgraph, Vector2)`, `CreateLocalSubgraphNode(name, Vector2)` +- `virtual OnEnable()`, `virtual OnDisable()`, `virtual OnGraphChanged(GraphLogger)` +- `UndoBeginRecordGraph(string actionName)`, **6.7** `UndoBeginRecordGraph(string actionName, params Node[] nodesToRecord)`, + `UndoEndRecordGraph()` +- `virtual bool IsConnectionAllowed(IPort output, IPort input)` +- **6.7** `SupportedTypes`, `AvailableVariableTypes`, `AvailableConstantTypes`, and the overrides + `protected virtual IEnumerable BuildAvailableVariableTypes(IReadOnlyCollection baseSupportedTypes)` + and `BuildAvailableConstantTypes(...)` to restrict the Blackboard type list +- `protected virtual OnDefineSubgraphNodeOptions(Node.IOptionDefinitionContext)` + +`OnEnable` runs when the asset is created, loaded, reopened, or after a domain reload. Mutating the +graph in `OnEnable`, `OnDisable` or `OnGraphChanged` throws `InvalidOperationException`. + +## 2. Nodes + +```csharp +[Serializable] +[Node("Math/Basic", "Icons/Add.png", "Add", "Styles/AddNode.uss")] // all args after the first are optional +class AddNode : Node +{ + protected override void OnDefinePorts(IPortDefinitionContext context) + { + context.AddInputPort("a").Build(); + context.AddInputPort("b").WithDefaultValue(1f).Build(); + context.AddOutputPort("result").Build(); + } +} +``` + +`NodeAttribute(categoryPath[, iconPath[, title[, stylesheet]]])`. The category path is a folder path +in the Add menu; the node's name is appended to it, so `"Math/Basic"` lists the node under +`Math/Basic/`. Icons: provide `name.png` and `d_name.png` (dark theme), 128 px or larger. + +`Node` members: `Graph`, `Hash128 ID`, `Title`, `Subtitle`, `Tooltip`, `Color DefaultColor`, +`float FillAmount`, `Vector2 Position`, `RemoveFromGraph()`, +`GetInputPorts()`, `GetInputPort(int)`, `GetInputPortByName(string)`, `InputPortCount`, +`GetOutputPorts()`, `GetOutputPort(int)`, `GetOutputPortByName(string)`, `OutputPortCount`, +`NodeOptions`, `GetNodeOption(int)`, `GetNodeOptionByName(string)`, `NodeOptionCount`. + +Overrides: `OnEnable()`, `OnDisable()`, `OnDefinePorts(IPortDefinitionContext)`, +`OnDefineOptions(IOptionDefinitionContext)`, **6.7** `OnPortDataTypeChanged(IPort, Type previous, Type next)`. +Set a dynamic default title in `OnEnable` with `Title = ...`. + +Discovery: node classes in the graph's assembly are listed automatically unless the graph sets +`GraphOptions.DisableAutoInclusionOfNodesFromGraphAssembly`. Nodes elsewhere use +`[UseWithGraph(typeof(MyGraph), typeof(OtherGraph))]`. Abstract and open generic classes are skipped. + +## 3. Ports + +`IPortDefinitionContext`: + +- `IInputPortBuilder AddInputPort(string portName)`, `IInputPortBuilder<T> AddInputPort<T>(string)` +- `IOutputPortBuilder AddOutputPort(string portName)`, `IOutputPortBuilder<T> AddOutputPort<T>(string)` + +Builder methods, chain then finish with `.Build()` which returns the `IPort`: + +| Method | Available on | Effect | +|---|---|---| +| `WithDisplayName(string)` | all | Label shown on the node; the constructor name stays the lookup key | +| `WithTooltip(string)` | all | Hover text | +| `WithConnectorUI(PortConnectorUI.Circle | Arrowhead)` | all | Connector shape | +| `AsVertical()` | all | Top/bottom placement; labels hidden; not on block nodes | +| `WithCapacity(PortCapacity.None | Single | Multi)` | all | Max wire count | +| `WithDataType<T>()` / `WithDataType(Type)` | untyped builders | Give a type after the fact | +| **6.7** `WithDataTypes(params Type[])` / `WithDataTypes(IEnumerable<Type>)` | untyped builders | Polymorphic port that accepts any of the listed types; at least one type, duplicates ignored | +| `WithDefaultValue(T)` | typed input | Value used when unconnected; editable on the node | +| `Delayed()` | input | Commit on Enter or focus loss | +| `AsTextArea(minLines, maxLines)` | input (string) | Multi-line field | + +A port built without a type has `DataType == typeof(Untyped)`; use it for execution flow. + +`IPort`: `Type DataType`, `Name`, `DisplayName`, `Tooltip`, `PortDirection Direction`, +`bool IsConnected`, `IPort FirstConnectedPort`, `GetConnectedPorts(List<IPort>)`, +`bool TryGetValue<T>(out T)`, `bool TrySetValue<T>(T)`, `Hash128 ID`, and the extension +`port.GetNode()`. + +`TryGetValue` returns the embedded value of an unconnected input; when connected, follow +`FirstConnectedPort.GetNode()` upstream instead. + +## 4. Node options + +Options are fields shown on the node body and in the Graph Inspector. + +```csharp +const string k_Count = "Count"; + +protected override void OnDefineOptions(IOptionDefinitionContext context) +{ + context.AddOption<int>(k_Count).WithDisplayName("Input count").WithDefaultValue(2).Delayed(); + context.AddOption<Mode>("Mode").ShowInInspectorOnly(); +} + +protected override void OnDefinePorts(IPortDefinitionContext context) +{ + GetNodeOptionByName(k_Count).TryGetValue<int>(out var count); + for (var i = 0; i < count; i++) + context.AddInputPort<float>($"in{i}").Build(); +} +``` + +`IOptionDefinitionContext`: `AddOption<T>(string name)`, `AddOption(string name, Type dataType)`. +Builder: `WithDisplayName`, `WithTooltip`, `WithDefaultValue`, `Delayed`, `AsTextArea`, +`ShowInInspectorOnly`, `Build()` (optional; the option is registered without it). +Option names must be unique on the node. +`INodeOption`: `DataType`, `Name`, `DisplayName`, `Tooltip`, `TryGetValue<T>`. Options are +user-edited; set the initial value with `WithDefaultValue`, not `TrySetValue`. + +Ports are rebuilt whenever an option changes, so `OnDefinePorts` must be a pure function of the +option values. + +## 5. Context and block nodes + +```csharp +[Serializable] class OnUpdateContext : ContextNode { } + +[Serializable] +[UseWithContext(typeof(OnUpdateContext))] // several context types allowed; derived contexts qualify +class LogBlock : BlockNode +{ + protected override void OnDefinePorts(IPortDefinitionContext context) + { + context.AddInputPort<string>("Message").Build(); + } +} +``` + +`ContextNode`: `BlockCount`, `BlockNodes`, `GetBlock(int)`, `AddBlockNode(BlockNode)`, +`InsertBlockNode(int, BlockNode)`, `CreateBlockNode<T>(int index = -1)`, `RemoveBlockNode`, +`ClearBlockNodes()`. `BlockNode`: `ContextNode`, `Index`. Block ports are always horizontal. + +## 6. Subgraphs + +- Enable with `[Graph(ext, GraphOptions.SupportsSubgraphs)]` on the main graph. +- Without further attributes the main graph type doubles as the subgraph type. To use dedicated + subgraph classes, mark each with `[Subgraph(typeof(MainGraph))]`; several are allowed and each gets + its own "Create <Name> Subgraph from Selection" action. +- Two flavours: asset subgraphs (a separate `.ext` file, `AddSubgraphNode(graph, position)`) and + local subgraphs stored inside the parent (`CreateLocalSubgraphNode<TSub>(name, position)`). +- The subgraph node's ports come from the subgraph's Blackboard variables with `VariableKind.Input` + or `VariableKind.Output`. +- `ISubgraphNode.GetSubgraph()` returns the nested `Graph`. Override + `OnDefineSubgraphNodeOptions` on the main graph to add options to subgraph nodes. + +## 7. Variables and constants + +- `graph.CreateVariable<float>("Speed", 1f, VariableKind.Local)` declares a Blackboard variable. + `VariableKind`: `Local`, `Input`, `Output`. +- `IVariable`: `Name`, `DataType`, `VariableKind`, `IsConnected`, `NodeCount`, `Graph`, **6.7** `StateMachine`, `Hash128 ID`, + `TryGetDefaultValue<T>`, `TrySetDefaultValue<T>`, `GetNodes(List<IVariableNode>)`, + `RemoveFromGraph(bool forceRemove)`. +- `graph.AddVariableNode(variable, position)` drops a variable onto the canvas; `IVariableNode.Variable`. + **6.7** adds the `VariableNodeMode.Get | Set` overload and `IVariableNode.Mode`. +- `graph.CreateConstantNode<int>(position, 5)`; `IConstantNode.DataType`, `TryGetValue`, `TrySetValue`. +- **6.7** Restrict the type dropdown by overriding `BuildAvailableVariableTypes` and + `BuildAvailableConstantTypes`; `Graph.SupportedTypes` lists the defaults. On 6.6 the Blackboard + type list is not customizable from the public API. + +## 8. Validation with OnGraphChanged + +```csharp +public override void OnGraphChanged(GraphLogger logger) +{ + var starts = GetNodes().OfType<StartNode>().ToList(); + if (starts.Count == 0) + logger.LogError("Add a Start node."); // marker on the graph + foreach (var extra in starts.Skip(1)) + logger.LogWarning("Only one Start node is used.", extra, // marker on the node + new GraphLogAction("Remove this node", n => + { + UndoBeginRecordGraph("Remove extra Start"); + RemoveNode((INode)n); + UndoEndRecordGraph(); + })); + + // 6.7 and newer only: the change delta. Added and modified nodes are reported; removed nodes are + // not, only removed ports are (inside ChangedPorts with ChangeKind.Removed). + foreach (var change in logger.GraphChanges.ChangedNodes) + if (change.Node is ObjectiveNode objective) { /* added or modified: check it */ } +} +``` + +`GraphLogger`: `LogError`, `LogWarning`, `Log`, each as `(object message, object context = null)` or +`(message, context, GraphLogAction)`. Context may be a node, a port, or null for the whole graph. +Messages also go to the Console. `GraphLogAction(string description, Action<object> action)` shows a +fix button on the marker. + +**6.7** `GraphLogger.GraphChanges` and `ChangeKind` are the change delta. They do not exist on 6.6. +The delta lists added and modified elements only; removed nodes, variables and subgraph nodes are +not reported (removed ports are, under `ChangedNode.ChangedPorts`). So on every version, detecting a +removed node means keeping a `[NonSerialized] HashSet<Hash128>` of node IDs on the graph and diffing +it against `GetNodes()` each call; on 6.6 that diff is also the only way to find additions. + +`GraphChanges`: `ChangedNodes`, `ChangedVariables`, `ChangedConstantNodes`, `ChangedSubgraphNodes`. +Entry types and their element property: `ChangedNode.Node` (`INode`), `ChangedVariable.Variable`, +`ChangedConstantNode.ConstantNode`, `ChangedSubgraphNode.SubgraphNode`; each also has `Hash128 ID` and +`ChangeKind ChangeKinds`, and `ChangedNode` adds `ChangedPorts` (`ChangedPort.Port`, `ID`, `ChangeKinds`). +`ChangeKind` flags: `Layout`, `Style`, `Data`, `Topology`, `Grouping`, `Added`, `Removed`, +`PortChanged`, `RecreateView`. + +## 9. Type casting between ports + +```csharp +public override bool IsConnectionAllowed(IPort output, IPort input) +{ + if (output.DataType == typeof(int) && input.DataType == typeof(float)) return true; + return base.IsConnectionAllowed(output, input); +} +``` + +The graph only records that the wire exists; the importer or runtime performs the conversion. +**6.7** `IPort.TrySetDataType(Type)` and `Node.OnPortDataTypeChanged` support ports whose type follows +a connection. + +## 10. Toolbar elements + +```csharp +[GraphToolbarElement("MyTool/Bake", typeof(MyGraph), order: 200)] +class BakeButton : EditorToolbarButton, IAccessContainerWindow +{ + public EditorWindow containerWindow { get; set; } + + public BakeButton() + { + text = "Bake"; + clicked += () => + { + if (containerWindow is IGraphWindow window && window.Graph is MyGraph graph) + Bake(graph); + }; + } +} +``` + +`UnityEditor.Toolbars.EditorToolbarButton` and `EditorToolbarToggle` both work. `IGraphWindow.Graph` +gives the open graph. + +## 11. Context menus (6.7) + +```csharp +[GraphMenu(typeof(MyGraph))] +static void AddGraphMenuEntries(GraphMenuContext context) +{ + if (context.ClickedObject is INode node) + context.AppendAction("Inspect node", () => Debug.Log(node.Title)); + context.AppendSeparator(); +} +``` + +`MenuContext`: `object ClickedObject`, `Vector2 MousePosition`, `AppendAction(name, Action)`, +`AppendAction(name, Action<DropdownMenuAction>[, status callback])`, `AppendSeparator(subMenuPath)`. +`[BlackboardMenu(typeof(MyGraph))]` receives the same context for the Blackboard. + +## 12. Node look: colors, accent, USS, custom UI + +- `Node.DefaultColor` sets the accent bar color; `Node.FillAmount` fills it as a progress bar. +- `[Node(category, icon, title, stylesheet)]` applies a USS file to the node's element. +- **6.7**: `class MyNodeView : NodeView<MyNode>` adds UI Toolkit elements. Allocate in + `OnViewBuilt()` and append to `View.Root`; re-add in `OnCullingChanged(false)`; never allocate in + `OnViewAttached()`, which fires repeatedly. Also `OnViewDetached`, `OnViewLODChanged(float zoom)`. + Discovery is by the generic argument, walking up the node inheritance chain. + +## 13. Wires + +`graph.GetWire(output, input)` returns a `Wire` with `OutputPort`, `InputPort`, `Graph`, +`float WidthOverride`, `float Opacity`, `bool IsDashed`. These are authoring-time styles; for runtime +styling use `WireReference` in `runtime-and-visualization.md`. diff --git a/skills/build-gtk/references/pitfalls.md b/skills/build-gtk/references/pitfalls.md new file mode 100644 index 0000000..bd1cf35 --- /dev/null +++ b/skills/build-gtk/references/pitfalls.md @@ -0,0 +1,86 @@ +# Pitfalls that survive compilation, and the ones that do not + +Check every row before handing off. File paths in this document resolve to this file's folder +(`references/`); `SKILL.md` is one level up. + +## Contents + +- Wrong API generation +- Discovery and menus +- Ports and options +- Lifecycle and mutation +- Serialization +- Custom UI (6.7) +- Visualization +- Importers + +## Wrong API generation + +| Symptom | Cause | Fix | +|---|---|---| +| Code uses `GraphModel`, `NodeModel`, `StateModel`, `GraphObject`, `GraphTool`, `GraphViewEditorWindow`, `Stencil`, `BlackboardGraphModel`, `GraphElementsExtensionMethodsCache`, `ModelView`, or `using Unity.GraphToolkit.ItemLibrary.Editor` | Retired Graph Tools Foundation API, internal implementation, or the 2025 experimental package; none is accessible from user code | Rewrite against `Graph`, `Node`, `State`, `GraphDatabase`, `StateMachineDatabase` and the builders in `graph-api.md` | +| `using UnityEditor.Experimental.GraphView;`, `GraphView`, `Port.Create`, `Edge`, `graphViewChanged`, `NodeCreationRequest` | The older GraphView API; Graph Toolkit is not a drop-in replacement and has no hand-written view code | Model the tool as `Graph` and `Node` classes and let the window be provided; change tracking moves to `OnGraphChanged` | +| The answer tells the user to install `com.unity.graphtoolkit` | Confusion with the experimental package; the module is built in from 6000.4 and that package name is a deprecated shim | Remove the install step; check `ProjectVersion.txt` instead | +| `StateMachine`, `State`, `Condition<T>` fail to resolve on a 6000.6 project | The State Machine API is 6000.7 and newer | Tell the user; do not fake the types | + +## Discovery and menus + +| Symptom | Cause | Fix | +|---|---|---| +| A node or state never appears in the Add menu | The class is `abstract`, or an open generic, or in another assembly without `[UseWithGraph]` / `[UseWithStateMachine]`, or the graph sets `DisableAutoInclusionOf...FromGraphAssembly`, or a block node lacks `[UseWithContext]` | Make the class concrete and closed, add the matching `UseWith...` attribute, or drop the flag | +| A node appears in an odd folder such as `Math/Add/Add` | `[Node("Math/Add")]` treats the whole first argument as a category path; the title is appended after it | `[Node("Math", null, "Add")]`, or set `Title` in `OnEnable` | +| A base node type the user never meant to expose is listed | It is concrete | Mark shared base classes `abstract` | + +## Ports and options + +| Symptom | Cause | Fix | +|---|---|---| +| A port is defined but nothing shows on the node | The builder chain lacks `.Build()` | End every chain with `.Build()` | +| `GetInputPortByName` or `GetNodeOptionByName` returns the wrong element | Two ports, or two options, share a name; names are lookup keys | Give every port and option a unique name | +| Ports rebuild on every keystroke of an integer option; typing "12" flashes 1 then 12 inputs | The option commits on each character | `.Delayed()` on the option | +| `GetNodeOptionByName(...)` returns null inside `OnDefinePorts` | The name differs from the one in `OnDefineOptions`, or the option is defined after it is read | Share a `const` for the name and define options before ports read them | +| `TryGetValue` on a connected input returns the stale embedded value | `TryGetValue` reads the node's own field, not the wire | When `IsConnected`, follow `FirstConnectedPort.GetNode()` upstream | +| Designers connect several wires to a port meant for one | Ports accept multiple wires by default | `.WithCapacity(PortCapacity.Single)` | +| `AsVertical()` has no effect | The node is a `BlockNode`; block ports are always horizontal | Keep block ports horizontal, or use a regular node | + +## Lifecycle and mutation + +| Symptom | Cause | Fix | +|---|---|---| +| `InvalidOperationException: Cannot change the graph in OnEnable, OnDisable and OnGraphChanged` | Graph mutation inside a locked callback | Move the mutation to a menu item, toolbar button, `GraphLogAction` or importer, wrapped in `UndoBeginRecordGraph` / `UndoEndRecordGraph` | +| Programmatic edits vanish on reopen | `GraphDatabase.SaveGraph` was not called, or `LoadGraph` was used inside an importer | Call `SaveGraph` after editing; use `LoadGraphForImporter` in importers | +| Undo history is polluted or Ctrl+Z does nothing after a generator ran | Missing `UndoBeginRecordGraph` / `UndoEndRecordGraph` pair | Bracket the whole batch in one pair | +| `OnEnable` runs several times | Expected: it fires on load, reopen and domain reload | Keep `OnEnable` idempotent | + +## Serialization + +| Symptom | Cause | Fix | +|---|---|---| +| Nodes disappear or reset after reopening the asset or a domain reload | A node, state, condition or self transition class lacks `[Serializable]`, or a graph field is not serializable | Add `[Serializable]` to every authoring class; use serializable field types | +| Compile error on a class declaration decorated with `[SerializeReference]` | `SerializeReference` is a field attribute | Classes take `[Serializable]`; use `[SerializeReference]` only on polymorphic fields in the runtime data model | +| `OnEnable` override on a `Condition` does not compile | `Condition` has no lifecycle callbacks; only `Graph`, `StateMachine`, `Node`, `State` and `SelfTransition` expose `OnEnable` / `OnDisable` | Remove the override; initialise in the condition's field initialisers or view | +| A field on a state machine that must not persist is written to the asset | Serializable fields on the class are saved | `[NonSerialized]` on the field, or `[field: NonSerialized]` on the property | +| Build errors about `Unity.GraphToolkit.Editor` in the player | Authoring types referenced from a runtime assembly | Move them to an Editor assembly; guard visualization code with `#if UNITY_EDITOR` | + +## Custom UI (6.7) + +| Symptom | Cause | Fix | +|---|---|---| +| Duplicated labels or buttons accumulate on a node or state | UI allocated in `OnViewAttached`, which fires on every re-attach | Allocate in `OnViewBuilt`, cache, and re-add in `OnCullingChanged(false)` | +| The custom element disappears after zooming far out and back | Culling clears `Root` | Re-add the cached element in `OnCullingChanged(false)` | +| Built-in condition row fields missing or misplaced | The `ConditionView<T>` removed or reparented them | Only append to `Root`; hide built-ins with `DisplayValueField` / `DisplayTitleLabel` | + +## Visualization + +| Symptom | Cause | Fix | +|---|---|---| +| `Motion.Play` and previews do nothing | The context was created with the runtime asset's own ID instead of the authored `Graph.ID`; or the graph window is not open (`IsGraphLoaded` is false); or the `*Enabled` toggle is off; or the context was disposed | Create the context with `Graph.ID`, check `IsValid` and `IsGraphLoaded`, and keep the toggles on while running | +| Previews from the previous step stay on screen | Previews persist until cleared | `ClearPreview()` when leaving a node, or `ClearAllVisualization()` at run end | + +## Importers + +| Symptom | Cause | Fix | +|---|---|---| +| The `.ext` asset shows as a generic Graph Toolkit asset instead of the runtime type | `ctx.SetMainObject(runtime)` missing | Call `SetMainObject` after `AddObjectToAsset` | +| Importer changes have no effect on existing assets | Import results are cached per importer version | Bump the `ScriptedImporter` version number, or reimport | +| Importer throws on an empty graph | No entry node to start from | Return early when the entry node is missing; `OnGraphChanged` already reports it | diff --git a/skills/build-gtk/references/runtime-and-visualization.md b/skills/build-gtk/references/runtime-and-visualization.md new file mode 100644 index 0000000..454413a --- /dev/null +++ b/skills/build-gtk/references/runtime-and-visualization.md @@ -0,0 +1,196 @@ +# From authored graph to runtime, and back + +Graph Toolkit has no runtime. The pattern used by every official sample is: a `ScriptedImporter` +registered on the graph's extension compiles the authored graph into a plain runtime asset at import +time, the game loads that asset, and, optionally, editor-only code mirrors execution back into the +open graph window through the GraphVisualization API. + +## Contents + +1. Assembly layout +2. The importer +3. Walking nodes and ports +4. Runtime data model +5. GraphVisualization: live debug view (6.6+) +6. Building graphs from code + +## 1. Assembly layout + +- `Editor/` (or an Editor-only asmdef): graph, nodes, importer, toolbar, views. Graph Toolkit is an + Editor module, so its assemblies are available to editor code like `UnityEditor` itself: neither a + plain `Editor` folder nor an Editor-only asmdef needs a reference entry for it. The official + samples' editor asmdefs use `IPort` and `PortCapacity` with an empty `references` list. +- `Runtime/` asmdef: the runtime data model and executor. Must not reference + `Unity.GraphToolkit.Editor`. Visualization calls live here under `#if UNITY_EDITOR`, because they + need the editor to be running the game. +- If the runtime asset type is referenced from the importer, the Editor asmdef references the Runtime + asmdef, never the reverse. + +## 2. The importer + +```csharp +using Unity.GraphToolkit.Editor; +using UnityEditor.AssetImporters; + +[ScriptedImporter(1, DialogueGraph.AssetExtension)] // bump the version to force reimport +class DialogueGraphImporter : ScriptedImporter +{ + public override void OnImportAsset(AssetImportContext ctx) + { + var graph = GraphDatabase.LoadGraphForImporter<DialogueGraph>(ctx.assetPath); + if (graph == null) + { + Debug.LogError($"Failed to load dialogue graph: {ctx.assetPath}"); + return; + } + + var runtime = ScriptableObject.CreateInstance<DialogueRuntimeGraph>(); + runtime.GraphId = graph.ID; + Build(graph, runtime); + + ctx.AddObjectToAsset("Runtime", runtime); + ctx.SetMainObject(runtime); // the .ext file now behaves as a DialogueRuntimeGraph in inspectors + } +} +``` + +- Use `LoadGraphForImporter`, not `LoadGraph`, inside importers. The state machine equivalent is + `StateMachineDatabase.LoadStateMachineForImporter<T>`. +- A null graph means the path, type or file is wrong; log and return. +- Validation errors belong in `OnGraphChanged`, which already shows markers; the importer only needs + to bail out quietly when the graph is invalid. +- Re-import happens on save. Sub-assets the game needs (textures, meshes) are added with further + `AddObjectToAsset` calls. + +## 3. Walking nodes and ports + +```csharp +static INode Next(INode node, string outputPortName) +{ + var port = node.GetOutputPortByName(outputPortName); + return port?.FirstConnectedPort?.GetNode(); // null when unconnected +} + +static T ValueOf<T>(INode node, string inputPortName) +{ + var port = node.GetInputPortByName(inputPortName); + if (port.IsConnected) + { + var source = port.FirstConnectedPort.GetNode(); // resolve upstream: constant node, variable node, or your node + if (source is IConstantNode constant && constant.TryGetValue<T>(out var c)) return c; + if (source is IVariableNode variable && variable.Variable.TryGetDefaultValue<T>(out var d)) return d; + } + port.TryGetValue<T>(out var embedded); // the value typed on the node + return embedded; +} +``` + +- Start from a dedicated entry node type: `graph.GetNodes().OfType<StartNode>().FirstOrDefault()`. +- Ports with `PortCapacity.Multi` need `GetConnectedPorts(list)` rather than `FirstConnectedPort`. +- Record `node.ID`, `port.ID` and wire endpoints (`outputPort.ID`, `inputPort.ID`) in the runtime + asset when a debug view is planned; the visualization API is keyed by these `Hash128` values. +- Block nodes: iterate `contextNode.BlockNodes`; a block's `Index` is its order. +- Subgraph nodes: `((ISubgraphNode)node).GetSubgraph()` returns the nested `Graph`; recurse. + +## 4. Runtime data model + +Keep it minimal and Unity-serializable: a `ScriptableObject` with a list of `[Serializable]` node +records or `[SerializeReference]` polymorphic nodes, referencing each other by index or `Hash128`. +Store only what execution needs; positions, titles and colors stay in the authoring asset. The +Visual Novel Director sample is the reference layout: `Runtime/` asmdef with the data model and a +`MonoBehaviour` executor that walks the records. + +## 5. GraphVisualization: live debug view (6.6+) + +Namespace `Unity.GraphToolkit.Editor.GraphVisualization`, editor-only. + +```csharp +#if UNITY_EDITOR +using Unity.GraphToolkit.Editor.GraphVisualization; +#endif + +class DialogueRunner : MonoBehaviour +{ +#if UNITY_EDITOR + Context m_Debug; +#endif + + void OnEnable() + { +#if UNITY_EDITOR + m_Debug = Registry.CreateVisualizationContext(runtimeGraph.GraphId); // the authored Graph.ID +#endif + } + + void OnDisable() + { +#if UNITY_EDITOR + m_Debug?.Dispose(); +#endif + } + + void Enter(DialogueRuntimeNode node) + { +#if UNITY_EDITOR + if (m_Debug is { IsValid: true, IsGraphLoaded: true }) + { + var nodeRef = m_Debug.GetNodeReference(node.Id); + m_Debug.Motion.Play(nodeRef, 1f); // animate the accent bar + m_Debug.GetPortReference(node.TextPortId).SetPreview(node.Text); + var wire = m_Debug.GetWireReference(node.OutPortId, node.NextInPortId); + wire.WidthOverride = 6f; + m_Debug.Motion.Play(wire, 1f); + } +#endif + } +} +``` + +`Registry`: `Context CreateVisualizationContext(Hash128 graphID)`, `Context GetActiveContext(Hash128 graphID)`. + +`Context` (`IDisposable`): `IsValid`, `IsGraphLoaded`, `GraphMotion Motion`, +`GetNodeReference(Hash128)`, `GetPortReference(Hash128)`, `GetWireReference(Hash128 outputPortID, Hash128 inputPortID)`, +`GetStateReference(Hash128)` (6.7), `GetTransitionReference(Hash128)` (6.7), `GetConditionReference(Hash128)` (6.7), +toggles `NodeCustomizationEnabled`, `PortPreviewEnabled`, `WireCustomizationEnabled`, +`StateCustomizationEnabled`, `TransitionCustomizationEnabled`, `ConditionCustomizationEnabled`, +and `ClearAllVisualization()`. + +- `NodeReference`: `FillAmount`, `ClearCustomization()`. +- `PortReference`: `SetPreview(string)`, `TryGetPreview(out string)`, `ClearPreview()`. +- `WireReference`: `IsDashed`, `WidthOverride`, `Opacity`, `ClearCustomization()`. +- `GraphMotion`: `Play(ref, float speed = 1f)`, `Stop(ref)`, `Pause(ref)` for node, wire, state and + transition references. + +Rules of thumb from the samples: create one context per run and dispose it when the run ends; clear +previews when the flow moves on so they do not pile up; leave traversed wires thicker but stopped to +show the path taken; add a `[GraphToolbarElement]` toggle that flips the `*Enabled` properties so the +debug view can be switched off. + +## 6. Building graphs from code + +Generators, migrations and tests build assets without the UI: + +```csharp +var graph = GraphDatabase.LoadGraph<DungeonGraph>(path) ?? GraphDatabase.CreateGraph<DungeonGraph>(path); +graph.UndoBeginRecordGraph("Generate dungeon"); + +var start = new StartNode { Position = new Vector2(0, 0) }; +graph.AddNode(start); +start.GetInputPortByName("Prefab").TrySetValue(startRoomPrefab); + +var room = new EncounterNode { Position = new Vector2(300, 0) }; +graph.AddNode(room); +graph.Connect(start.GetOutputPortByName("Output"), room.GetInputPortByName("Input")); + +var enemies = graph.CreateVariable<GameObject>("Enemy"); +var enemyNode = graph.AddVariableNode(enemies, new Vector2(300, 150)); +graph.Connect(enemyNode.GetOutputPort(0), room.GetInputPortByName("Enemy")); + +GraphDatabase.SaveGraph(graph); +graph.UndoEndRecordGraph(); +``` + +Ports exist as soon as the node is added to the graph, because adding triggers `OnDefinePorts`. +Set `Position` before or after adding; both work. State machines follow the same shape with +`AddState`, `Connect(fromState, toState)`, `SaveStateMachine` and the `UndoBeginRecordStateMachine` +pair. diff --git a/skills/build-gtk/references/state-machine-api.md b/skills/build-gtk/references/state-machine-api.md new file mode 100644 index 0000000..5b1831a --- /dev/null +++ b/skills/build-gtk/references/state-machine-api.md @@ -0,0 +1,286 @@ +# State Machine API reference (Unity 6.7 and newer) + +All types live in `Unity.GraphToolkit.Editor`. None of this exists on Unity 6000.6 or older: the +public Script Reference for these types starts at +`https://docs.unity3d.com/<VERSION>/Documentation/ScriptReference/Unity.GraphToolkit.Editor.StateMachine.html` +with `<VERSION>` at least `6000.7`. +There is no manual walkthrough page yet; the Script Reference is the authoritative source and this file is the map of it. + +## Contents + +1. Concepts +2. StateMachine class and asset lifecycle +3. States +4. Transitions and rules +5. Conditions +6. Custom self transitions +7. Variables +8. Validation with OnStateMachineChanged +9. Context menus +10. Custom UI for states and conditions +11. Subgraph states +12. Reading a state machine for a runtime + +## 1. Concepts + +- A `StateMachine` is a graph whose nodes are `State`s and whose wires are transitions. The state + machine owns transitions; states have no ports to define. +- A transition connects a `FromState` to a `ToState`. A **self transition** has the same state on both + ends and is anchored on it. +- A transition carries one or more **rules** (`ITransitionRule`). The transition is taken when the + conditions of any enabled rule hold. Each rule has a `RootCondition`, an `IGroupCondition` combining + child conditions with And/Or. +- Conditions are either built in (group, variable comparison) or user types derived from + `Condition<T>` / `Condition`. +- Everything about which state is active, when transitions fire, and what a condition means at + runtime is the user's code. Graph Toolkit stores the authoring data only. + +## 2. StateMachine class and asset lifecycle + +```csharp +[StateMachine(AssetExtension)] // optional second arg: StateMachineOptions flags +[Serializable] +class EnemyBrain : StateMachine +{ + public const string AssetExtension = "enemybrain"; + + [MenuItem("Assets/Create/AI/Enemy Brain")] + static void Create() => StateMachineDatabase.PromptInProjectBrowserToCreateNewAsset<EnemyBrain>(); +} +``` + +`StateMachineOptions` flags: `SupportsSubgraphs`, +`DisableAutoInclusionOfStatesFromStateMachineAssembly`. `Default` and `None` are 0. The flag applies +to states, conditions and self transitions alike. + +`StateMachineDatabase` (static): `PromptInProjectBrowserToCreateNewAsset<T>(string defaultName = "New State Machine")`, +`CreateStateMachine<T>(path)`, `LoadStateMachine<T>(path)`, `LoadStateMachineForImporter<T>(path)`, +`SaveStateMachine(StateMachine)`, `GetStateMachineAssetGUID`, `GetStateMachineAssetPath`. + +`StateMachine` members: + +- `Name`, `StateCount`, `VariableCount`, `Hash128 ID`, `GUID AssetGuid`, `AvailableVariableTypes` +- `GetStates()`, `GetState(int)`, `AddState(State)`, `RemoveState(IState)` +- `ITransition Connect(IState from, IState to)`: creates the transition with one rule, or returns the + existing transition after adding a rule to it. `Connect(s, s)` creates or extends a self transition. +- `bool Disconnect(IState from, IState to)`, `IEnumerable<ITransition> GetTransitions(IState from, IState to)` +- `CreateVariable<T>(...)`, `GetVariables()`, `GetVariable(int)`, `RemoveVariable(...)` +- `virtual OnEnable()`, `OnDisable()`, `OnStateMachineChanged(StateMachineLogger)` +- `UndoBeginRecordStateMachine(string actionName[, params Condition[] conditionsToRecord])`, + `UndoEndRecordStateMachine()` +- `protected virtual IEnumerable<Type> BuildAvailableVariableTypes()` + +Mutating in `OnEnable`, `OnDisable` or `OnStateMachineChanged` throws `InvalidOperationException`. + +## 3. States + +```csharp +[Serializable] +[State("Movement", "Icons/Patrol.png", "Patrol")] // args after the first are optional +class PatrolState : State +{ + protected override void OnDefineOptions(IOptionDefinitionContext context) + { + context.AddOption<float>("Speed").WithDefaultValue(2f).WithTooltip("Units per second"); + context.AddOption<string>("Notes").AsTextArea(); + } +} +``` + +`StateAttribute(categoryPath[, iconPath[, title[, stylesheet]]])` mirrors `NodeAttribute`. + +State options appear only in the Graph Inspector, never on the state itself. +`State.IOptionDefinitionContext`: `AddOption<T>(name)`, `AddOption(name, Type)`. Builder +`IStateOptionBuilder`: `WithDisplayName`, `WithTooltip`, `WithDefaultValue`, `Delayed`, `AsTextArea`, +`Build()`. Read back with `GetOptionByName(name).TryGetValue<T>(out v)`; `IStateOption` also has +`DataType`, `Name`, `DisplayName`, `Tooltip`, `TrySetValue<T>`. + +`State` / `IState` members: `StateMachine`, `Hash128 ID`, `Title`, `Subtitle`, `Tooltip`, +`Color DefaultColor`, `float FillAmount`, `Vector2 Position`, `bool IsConnected`, +`GetIncomingTransitions()`, `GetOutgoingTransitions()`, `Options`, `OptionCount`, `GetOption(int)`, +`GetOptionByName(string)`, `RemoveFromStateMachine()`, `OnEnable()`, `OnDisable()`. + +Discovery: states in the state machine's assembly are listed automatically; elsewhere use +`[UseWithStateMachine(typeof(EnemyBrain))]`. `[UseWithGraph]` is ignored by state machines. + +## 4. Transitions and rules + +Users create transitions by dragging from a state's edge (Shift + left mouse also starts one). In +code use `stateMachine.Connect(from, to)`. + +`ITransition`: `Hash128 ID`, `IState FromState`, `IState ToState`, `GetRules()`, `int RuleCount`, +`GetRule(int)`, `ITransitionRule AddRule()`, `AddRule(ITransitionRule)`, `RemoveRule(ITransitionRule)`, +`Texture2D Icon`, `Color FillColor` (arrow head), `Color LineColor` (line and arrow border), +`float WidthOverride`, `float Opacity`, `bool IsDashed`. +A transition always keeps at least one rule; removing the last one is refused. + +`ITransitionRule`: `Hash128 ID`, `string Title`, `bool Enabled`, `IGroupCondition RootCondition`, +`ITransition Transition`, `Texture2D Icon`. + +`IGroupCondition : ICondition`: `GroupConditionOperation Operation` (`And`, `Or`), `Count`, `Get()`, +`Get(int)`, `Add(ICondition)`, `Insert(int, ICondition)`, `Remove(ICondition)`, `Clear()`. + +## 5. Conditions + +```csharp +[Serializable] +[Condition("Distance to player")] // title in the Add menu of a rule +class DistanceCondition : Condition<float> +{ + public override string Tooltip => "Compared against the distance to the player."; + protected override bool DisplayComparisonDropdown => true; + protected override IReadOnlyList<ConditionComparison> SupportedComparisons => + new[] { ConditionComparison.Less, ConditionComparison.Greater }; +} + +[Serializable] +[Condition("Took damage")] +class TookDamageCondition : Condition { } // valueless trigger, label-only row +``` + +- `Condition<T>`: `T Value` (serialized), `ConditionComparison Comparison`, + `virtual bool DisplayComparisonDropdown` (default false), `virtual IReadOnlyList<ConditionComparison> SupportedComparisons`. +- `Condition`: `Hash128 ID`, `ITransitionRule Rule`, `StateMachine`, `Texture2D Icon`, + `virtual string Title`, `virtual string Tooltip`. +- `ConditionComparison`: `Equal`, `NotEqual`, `Less`, `LessOrEqual`, `Greater`, `GreaterOrEqual`. +- Built-in conditions, created in code with `Condition.CreateGroupCondition()` and + `Condition.CreateVariableCondition(IVariable variable, object value, ConditionComparison comparison)`. + Read them through `IGroupCondition` and `IVariableCondition` (`Variable`, `Comparison`, `Value`). +- Without a `[Condition]` attribute the type name is used as the title. Abstract and open generic + condition classes are not listed. +- Same discovery rules as states: same assembly, or `[UseWithStateMachine(...)]`. + +Editing a condition value from code: `UndoBeginRecordStateMachine("Set distance", condition)`, +assign `condition.Value`, `UndoEndRecordStateMachine()`, then `SaveStateMachine`. + +## 6. Custom self transitions + +```csharp +[Serializable] +[UseWithStateMachine(typeof(EnemyBrain))] +[Transition("", "Icons/Loop.png", "Repeat")] // (categoryPath[, iconPath[, title]]) +class RepeatTransition : SelfTransition +{ + public override void OnEnable() + { + base.OnEnable(); + LineColor = Color.cyan; + Tooltip = "Loops the state while its conditions hold."; + } +} +``` + +Custom self transitions appear in a state's Create Transition menu. `SelfTransition` exposes the +`ITransition` members plus `Tooltip`, `OnEnable`, `OnDisable`. Regular state-to-state transitions are +not subclassed; style them through `ITransition` properties. + +## 7. Variables + +`stateMachine.CreateVariable<int>("Health", 100)` declares a Blackboard variable that the built-in +variable condition can compare against. `IVariable.StateMachine` gives the owner. Variable kinds and +members are the same as in the Graph API. + +## 8. Validation with OnStateMachineChanged + +```csharp +public override void OnStateMachineChanged(StateMachineLogger logger) +{ + foreach (var state in GetStates()) + if (!state.GetOutgoingTransitions().Any() && state is not EndState) + logger.LogWarning("Dead end: add a transition out of this state.", state); + + foreach (var change in logger.StateMachineChanges.ChangedTransitions) + if ((change.ChangeKinds & ChangeKind.Added) != 0) { /* new transition */ } +} +``` + +`StateMachineLogger`: `LogError`, `LogWarning`, `Log`, each as `(object message, object context = null)` +or `(message, context, StateMachineLogAction)`. Context may be a state, a transition, or null for +the whole state machine. +`StateMachineLogAction(string description, Action<object> action)`. +`StateMachineChanges`: `ChangedStates`, `ChangedTransitions`, `ChangedVariables`, +`ChangedSubgraphStates`. Element properties: `ChangedState.State`, `ChangedTransition.Transition`, +`ChangedVariable.Variable`, `ChangedSubgraphState.SubgraphState`; each also has `Hash128 ID` and +`ChangeKind ChangeKinds`. + +## 9. Context menus + +```csharp +[StateMachineMenu(typeof(EnemyBrain))] +static void AddEntries(StateMachineMenuContext context) +{ + switch (context.ClickedObject) + { + case IState state: context.AppendAction("Log state", () => Debug.Log(state.Title)); break; + case ITransition transition: context.AppendAction("Log rules", () => Debug.Log(transition.RuleCount)); break; + default: context.AppendAction("Canvas action", () => { }); break; + } +} + +[ConditionMenu(typeof(EnemyBrain))] +static void AddConditionEntries(ConditionMenuContext context) // context.StateMachine, .Transition, .Rule +{ +} +``` + +## 10. Custom UI for states and conditions + +- `class PatrolView : StateView<PatrolState>`: `State`, `View.Root`, `OnViewBuilt()`, + `OnViewAttached()`, `OnViewDetached()`, `OnViewLODChanged(float)`, `OnCullingChanged(bool)`. + Allocate elements in `OnViewBuilt` and re-add them in `OnCullingChanged(false)`; never allocate in + `OnViewAttached`. +- `class DistanceView : ConditionView<DistanceCondition>`: `Condition`, `View.Root`, `OnViewBuilt()`, + `OnViewAttached()`, `OnViewDetached()`, `OnConditionChanged()`, and the toggles + `protected virtual bool DisplayValueField`, `DisplayTitleLabel`. Add to `Root`; do not remove or + reparent the built-in row elements. View instances are never serialized. +- Exceptions thrown by a view are logged and the built-in UI still renders. + +## 11. Subgraph states + +With `StateMachineOptions.SupportsSubgraphs`, a state can hold a nested state machine. +`ISubgraphState : IState` marks such states; `StateMachineChanges.ChangedSubgraphStates` reports them. + +## 12. Reading a state machine for a runtime + +```csharp +[ScriptedImporter(1, EnemyBrain.AssetExtension)] +class EnemyBrainImporter : ScriptedImporter +{ + public override void OnImportAsset(AssetImportContext ctx) + { + var sm = StateMachineDatabase.LoadStateMachineForImporter<EnemyBrain>(ctx.assetPath); + if (sm == null) { Debug.LogError($"Failed to load {ctx.assetPath}"); return; } + + var runtime = ScriptableObject.CreateInstance<RuntimeBrain>(); + foreach (var state in sm.GetStates()) + { + var rs = new RuntimeState { Id = state.ID, Name = state.Title }; + state.GetOptionByName("Speed")?.TryGetValue<float>(out rs.Speed); + foreach (var transition in state.GetOutgoingTransitions()) + foreach (var rule in transition.GetRules()) + if (rule.Enabled) + rs.Transitions.Add(new RuntimeTransition + { + Target = transition.ToState.ID, + Predicate = Compile(rule.RootCondition) + }); + runtime.States.Add(rs); + } + ctx.AddObjectToAsset("Runtime", runtime); + ctx.SetMainObject(runtime); + } + + static RuntimePredicate Compile(ICondition condition) => condition switch + { + IGroupCondition g => RuntimePredicate.Group(g.Operation, g.Get().Select(Compile)), + IVariableCondition v => RuntimePredicate.Variable(v.Variable.Name, v.Comparison, v.Value), + DistanceCondition d => RuntimePredicate.Distance(d.Comparison, d.Value), + TookDamageCondition => RuntimePredicate.Trigger("TookDamage"), + _ => RuntimePredicate.Never + }; +} +``` + +Identify the entry state by a dedicated `State` subclass or a state machine option; the API has no +built-in "default state". Keep `Hash128` IDs in the runtime asset if the GraphVisualization API will +highlight states later. diff --git a/skills/generate-editor-search-query/SECURITY.md b/skills/generate-editor-search-query/SECURITY.md index 75c09d3..467c145 100644 --- a/skills/generate-editor-search-query/SECURITY.md +++ b/skills/generate-editor-search-query/SECURITY.md @@ -15,7 +15,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Query passed base64-encoded, not interpolated as text.** The generated Unity Search query is base64-encoded before it is embedded in the C# snippet, and decoded back to a string inside the snippet. A query containing `"` can't break out of the C# string, and a query containing `'` can't break the shell's single-quoting around `--code` — base64's alphabet (`A-Z a-z 0-9 + / =`) contains neither character, so the query text can never change the code that runs. - **Read-only.** The snippet only opens the Search window; it does not select, rename, delete, move, import, edit, or save anything. - -## Background - -Pending owner review. diff --git a/skills/implement-in-app-purchases/README.md b/skills/implement-in-app-purchases/README.md deleted file mode 100644 index f714fd4..0000000 --- a/skills/implement-in-app-purchases/README.md +++ /dev/null @@ -1,233 +0,0 @@ -# Unity In-App Purchases Skill - -This skill helps you implement, configure, debug, and migrate Unity In-App Purchases (IAP) using `com.unity.purchasing` v5. It covers standard Apple App Store / Google Play billing, IAP D2C Capabilities (Direct-to-Customer — Stripe/Coda via Unity Cloud), and conversion from a wide range of third-party and native billing implementations. - ---- - -## What This Skill Covers - -| Path | When to use | -|---|---| -| **Add IAP to a new project** | No existing IAP — start from scratch with Unity IAP 5 | -| **Migrate v4 → v5** | Project uses `IStoreListener`, `UnityPurchasing.Initialize`, or `ConfigurationBuilder` | -| **Convert native Google BillingClient** | Project calls Android BillingClient via `AndroidJavaObject` / JNI bridge | -| **Convert native iOS StoreKit** | Project uses a custom ObjC/Swift StoreKit plugin bridged via `DllImport("__Internal")` | -| **Convert Essential Kit billing** | Project uses VoxelBusters Essential Kit for in-app purchases (without losing other features) | -| **Assess/Convert UniPay (FLOBUK)** | Project uses UniPay — determines whether migration is needed or possible | -| **Assess/Convert RevenueCat** | Project uses RevenueCat — evaluate/implement observer mode to work with Unity IAP 5 | -| **Assess/Convert Adapty** | Project uses Adapty — evaluate/implement observer mode to work with Unity IAP 5 | -| **Implement IAP D2C Capabilities** | Add third-party payment provider (Stripe or Coda) via Unity Cloud | - -The skill always scans the project first and routes to the correct path automatically. You can also specify a path manually if you know exactly which one you need. - ---- - -## Example Prompts - -``` -Add in-app purchases code to my game. I have a 100-coin pack and a Remove Ads unlock. -``` -``` -Migrate my existing IAP code in the project from Unity IAP v4 to v5. -``` -``` -My project uses AndroidJavaObject to call Google Play BillingClient. Convert it to Unity IAP. -``` -``` -I have a custom ObjC StoreKit plugin with SKPaymentQueue. Replace it with Unity IAP. -``` -``` -Disable Essential Kit billing and replace with Unity IAP 5. Keep other Essential Kit features working as before. -``` -``` -Can my RevenueCat project implement app store purchases through Unity's IAP v5? -``` -``` -My project uses Adapty. What features will we lose if we use Unity IAP instead? Give me a detailed report and do not make any changes now. -``` -``` -Add Stripe payment support via Unity IAP D2C Capabilities. -``` -``` -Add a web checkout option for my players using Unity's IAP D2C Capabilities with Coda. -``` - ---- - -## Path Details, Limitations, and Best Practices - -### Add IAP to a New Project - -**What it does:** Installs Unity IAP 5, creates an `IAPManager`, wires up the two-step purchase flow (pending → confirm), integrates with your existing save system, and connects to your shop UI. - -**Limitations:** -- The skill cannot create App Store Connect or Google Play Console product entries — you must do that manually. -- If you use local Google Play receipt validation, you must run the Receipt Validation Obfuscator manually in the Unity Editor (**Services > In-App Purchasing > Receipt Validation Obfuscator**). -- Apple local receipt validation is a no-op under StoreKit 2. Use server-side JWS validation for Apple. - -**Best practices:** -- Define all products in one authoritative catalog (a `ScriptableObject` or constants class) — not scattered across button click handlers. -- Always call `ConfirmPurchase` **after** saving the granted reward. An unconfirmed purchase re-delivers safely; a confirmed-but-unsaved one loses the reward permanently. -- Subscribe to all events **before** calling `Connect()` — pending purchases from a previous session may fire immediately on reconnect. -- Add a "Restore Purchases" button for any project with NonConsumable or Subscription products — required on iOS. - ---- - -### Migrate v4 → v5 - -**What it does:** Replaces the listener-based `IStoreListener` / `UnityPurchasing.Initialize` / `ConfigurationBuilder` pattern with the event-driven `StoreController` pattern. - -**Limitations:** -- `product.receipt` and `product.hasReceipt` are removed. The skill migrates ownership checks to `store.CheckEntitlement(product)` — verify your entitlement logic after migration. -- Apple local validation via `CrossPlatformValidator` still compiles but is a no-op under StoreKit 2. If your project validates Apple receipts locally, this validation silently stops working after migration. -- Developer payload (the third `Purchase()` argument) is removed. If your backend uses it, a backend change is required. -- `SubscriptionManager` is replaced — subscription info is now on `order.Info.PurchasedProductInfo`, not on `CartItem`. - -**Best practices:** -- Migrate one system at a time: get initialization working first, then purchase flow, then restore. -- Run the migration in a branch. The skill uses `#if` guards where needed, but test thoroughly before removing the old code. -- After migration, recheck all `OnPurchaseConfirmed` handlers — the event now receives `Order` (base type) and you must pattern-match `ConfirmedOrder` vs `FailedOrder`. - ---- - -### Convert Native Google BillingClient - -**What it does:** Converts C# → JNI → BillingClient bridge code to Unity IAP 5, preserves the public billing API via a compatibility facade, and wraps old native code in `#if !USE_UNITY_IAP_V5` for easy rollback. - -**Limitations:** -- **Multiple subscription base plans / offer tokens** (`basePlanId`, `offerToken`, `offerId`) are a hard blocker — Unity IAP does not expose offer-level selection. You must either simplify your Play Console subscription setup to one base plan per product, or stay on native billing. -- **Personalized price disclosure** (`setIsPersonalizedPrice`) and **alternative billing / external offers** are hard blockers. -- **Multi-quantity purchases** (quantity > 1 per transaction) are not supported in Unity IAP. -- A mixed native BillingClient + Unity IAP architecture is not supported — both compete for the same `PurchasesUpdatedListener` slot. The skill will not generate a mixed setup. -- The receipt format changes: the backend receives `order.Info.Receipt` (a JSON object containing `purchaseToken`, `orderId`, and `signature`) instead of raw fields. If your backend parses these fields individually, a backend update is required. - -**Best practices:** -- Run the blocker scan and read the migration report before making any changes. Hard blockers require a decision before code is written. -- Keep the Gradle billing dependency in place until migration is validated — Unity IAP brings its own BillingClient, but removing the old dependency prematurely can break the build in unexpected ways. -- Use the `#if USE_UNITY_IAP_V5` / `#if !USE_UNITY_IAP_V5` guards throughout. The rollback path (remove the define) must work cleanly before you ship. -- Test on a physical Android device with a real sandbox account — the BillingClient behavior in the Unity Editor and on emulators is not representative of production. - ---- - -### Convert Native iOS StoreKit - -**What it does:** Converts a custom Objective-C or Swift StoreKit plugin (bridged via `DllImport("__Internal")` and `UnitySendMessage`) to Unity IAP 5, preserving the C# game-facing API via a compatibility facade. - -**Limitations:** -- **Receipt format is a breaking backend change.** Unity IAP uses per-transaction JWS (StoreKit 2 style) rather than the SK1 base64 app receipt bundle. If your backend validates the receipt bundle against Apple's `/verifyReceipt` endpoint, it must migrate to Apple's App Store Server API or JWS validation before you can ship. -- **Promotional offer signing** (`SKPaymentDiscount` / SK2 signed offers) is a hard blocker — Unity IAP has no equivalent for server-signed promotional offers. -- **Win-back offers** (StoreKit 2) are not supported in Unity IAP 5.4. -- **`SKStorefront`** (region detection via storefront change observer) is not exposed in Unity IAP. -- **`SKReceiptRefreshRequest`** has no direct equivalent — `store.FetchPurchases()` covers the purchase re-delivery use case but not manual receipt refresh. -- ObjC/Swift plugin files cannot use C# `#if` defines. The skill guards only the C# call sites — the native files remain in `Assets/Plugins/iOS/` and compile unconditionally. -- A mixed native StoreKit + Unity IAP architecture is not supported — both register as `SKPaymentQueue` observers and only one reliably receives callbacks. - -**Best practices:** -- Audit the backend receipt validation endpoint before starting. If it calls `/verifyReceipt`, plan the backend migration in parallel with the client migration. -- If the plugin intercepts App Store promotional purchases (`shouldAddStorePayment`), wire up `OnPromotionalPurchaseIntercepted` and `ContinuePromotionalPurchases()` before removing the native interceptor. -- Test Ask-to-Buy explicitly — the deferred → pending two-stage flow behaves differently than a native `PURCHASING` → `PURCHASED` transition. -- Validate the migration on TestFlight, not just in the Unity Editor or Simulator. StoreKit behavior differs between Editor sandbox, Simulator, and TestFlight builds. - ---- - -### Convert Essential Kit Billing - -**What it does:** Disables the Essential Kit Billing service flag in `EssentialKitSettings.asset`, removes the conflicting `com.android.billingclient` Gradle dependency, and implements Unity IAP 5 using the product catalog extracted from the Essential Kit settings. - -**Limitations:** -- Essential Kit C# source files are **not deleted**. The Billing service is disabled via the settings flag — all EK code remains and compiles. Do not expect a clean removal. -- Only the Billing service is affected. All other Essential Kit services (Notification, GameServices, etc.) are left completely untouched. -- **Product IDs must not change.** The same IDs used in Essential Kit must be carried over to Unity IAP to preserve store history. -- If your project uses **server-side receipt validation**, the receipt format changes from `transaction.RawData` (Android) and `transaction.Receipt` (iOS JWS) to `order.Info.Receipt` and `order.Info.Apple?.jwsRepresentation`. Document the backend change required. -- Subscriptions are fully supported, but the restore path changes — EK's `OnRestorePurchasesComplete` maps to both `store.OnPurchasesFetched` and the `RestoreTransactions` callback in Unity IAP. - -**Best practices:** -- Run `Assets > External Dependency Manager > Android Resolver > Force Resolve` after removing the Gradle billing dependency — do not skip this step. -- Verify the Essential Kit Billing service is visually disabled in **Window > Voxel Busters > Essential Kit > Open Settings → Services** after the settings file edit. -- Confirm all product IDs in App Store Connect and Play Console match the Unity IAP catalog exactly before testing. - ---- - -### Assess UniPay (FLOBUK) - -**What it does:** This path does **not** perform a conversion. It assesses whether migration is needed and routes to one of three outcomes: unsupported platform (stop), upgrade required (stop), or no action needed (UniPay already wraps Unity IAP 5). - -**Limitations:** -- UniPay's Steam, Meta Quest, PayPal, and Facebook Instant Games integrations have **no Unity IAP equivalent**. If your project targets any of these platforms, there is no migration path — keep UniPay. -- If `com.unity.purchasing` is below v5.4, IAP D2C Capabilities (Stripe/Coda) are not available — an upgrade to the latest stable v5.4+ is needed before adding D2C support. -- The skill does not perform a "remove UniPay" conversion — that is a manual refactor scoped to what features you are replacing. - -**Best practices:** -- If the assessment concludes "no action needed," there is no code to write. Work within UniPay's API for new products or purchase logic changes. -- If you want to remove UniPay entirely and use Unity IAP directly, ask the skill explicitly and describe which UniPay features you are replacing — it will assess feasibility and scope. - ---- - -### Assess/Convert RevenueCat - -**What it does:** Evaluates whether the project can switch to Unity IAP 5 for handling purchases. Produces one of three outcomes: already in observer mode (no action), blockers detected (report + two choices), or no blockers (two conversion paths: observer mode or full removal). - -**Limitations:** -- **Amazon Appstore** is a hard blocker — Unity IAP 5 has removed Amazon support. RevenueCat observer mode also does not work reliably on Amazon builds. If your project targets Amazon, conversion is not viable. -- **RevenueCat Offerings / remote paywalls** have no Unity IAP equivalent — all products must be defined in code or a local catalog. -- **RevenueCat A/B testing (Experiments)** has no Unity IAP equivalent. -- **Cross-platform entitlement sync** (a user who buys on iOS retains access on Android) has no Unity IAP equivalent without a custom backend. -- **RevenueCat webhook events** (subscription renewals, cancellations, billing issues) are not delivered by Unity IAP — you would need to build your own subscription event infrastructure. -- In **observer mode**, `SyncPurchases()` must be called after every Unity IAP confirmed purchase. Missing this call means RevenueCat does not validate the receipt and `CustomerInfo` is not updated. - -**Best practices:** -- Run the full feature check before deciding. RevenueCat's value often comes from features that are not obvious in the codebase (e.g., webhooks configured server-side). -- If the project has a marketing team managing paywall copy or pricing remotely via RevenueCat Offerings, a full removal will require significant UI work to replace that capability. -- Observer mode is the lower-risk path — it preserves RevenueCat's server-side validation and subscription lifecycle tracking while Unity IAP handles the native purchase flow. - ---- - -### Assess/Convert Adapty - -**What it does:** Evaluates whether the project can switch to Unity IAP 5 for app store purchase handling. Produces one of three outcomes: already in observer mode (no action), blockers detected (report + two choices), or no blockers (observer mode or full removal). - -**Limitations:** -- **Adapty Paywall Builder** has no Unity IAP equivalent — and it is also **unavailable in Adapty's own Observer Mode**. Switching to observer mode loses Paywall Builder regardless of whether Unity IAP is involved. -- **Adapty A/B testing** has no Unity IAP equivalent. In observer mode, A/B testing is possible but requires significant manual instrumentation. -- **Cross-platform entitlement sync** has no Unity IAP equivalent without a custom backend. -- In **observer mode**, `Adapty.ReportTransaction()` must be called after every Unity IAP confirmed purchase. Missing this call means Adapty does not validate the receipt server-side. - -**Best practices:** -- If your marketing team uses Adapty's Paywall Builder to update paywall layouts without app releases, note that this capability is lost in observer mode. Make sure all stakeholders understand this before proceeding. -- Full removal requires replacing any `AdaptyUI` / `PaywallView` shop UI with custom Unity UI before Adapty can be removed. Budget time for this work. -- Observer mode is the lower-risk path and preserves Adapty's webhook delivery and analytics integrations. - ---- - -### Implement IAP D2C Capabilities - -**What it does:** Adds Direct-to-Customer (D2C) third-party payment provider support (Stripe or Coda) via Unity Cloud, including remote catalog setup, deep link configuration, the built-in payment options picker UI, Apple/Google external purchase compliance tools, and entitlement delivery guidance. - -**Limitations:** -- Requires **Unity IAP v5.4+**, **Unity Editor 2022.3+**, `com.unity.services.authentication` **v3.7.1+**, and `com.unity.services.core` **v1.18.0+**. All must be satisfied before any code is written. -- **Subscriptions are not supported** by IAP D2C Capabilities in v5.4. Only Consumable and NonConsumable products can be used. -- Requires a **Stripe or Coda account** connected in the Unity Cloud IAP dashboard, and Unity must enable D2C for your organization — contact your Unity Client Partner if it is not yet enabled. -- **Routing rules** must be configured in the Unity Dashboard before any player is offered a D2C payment option. Without a routing rule, no provider is offered even if a provider account is connected. -- **External web payments via Stripe/Coda are permitted in select regions only.** Apple and Google have their own program eligibility requirements. The developer is responsible for determining eligibility and meeting disclosure requirements — the skill does not perform compliance on your behalf. -- **Anonymous sign-in** must not be used as the authentication method. If a player's session token is lost (reinstall, app data clear), purchase history tied to an anonymous identity becomes unrecoverable. -- The **receipt format is different from standard IAP 5** — D2C purchases go through Unity Cloud, not the device's native store, so `order.Info.Apple` and `order.Info.Receipt` behave differently for D2C orders. - -**Best practices:** -- Set up **routing rules** in the Unity Dashboard before testing. Without them, `GetEligiblePaymentProviders()` returns an empty list and the purchase UI never appears — this is a common "nothing happens" issue during initial integration. -- Use a **proxy HTML page** for the Success Redirect URL rather than a direct app-scheme URL. On some Android and iOS devices, direct app-scheme redirects from the payment provider domain are silently dropped. Host the proxy on a stable HTTPS domain you control. -- Use **`ShowPurchaseOption(catalogListingId)`** as the primary purchase entry point — it shows the built-in picker UI and handles provider selection automatically. Only fall back to `PurchaseProduct` directly when `GetEligiblePaymentProviders()` returns an empty `Providers` list. -- Configure a **Cancel Redirect URL** in the payment provider dashboard — without it, the checkout page has no "Back" button and players who change their mind are stuck in the browser. -- The SDK remembers the **last used payment provider per player per device** (provider memory). This is expected behavior, not a bug. Clear app data to reset it during testing. -- Deploy the **Deployment package** (`com.unity.services.deployment`) early — it is required to push `.ucat` product definitions to the Remote Catalog. Without it, the catalog cannot be deployed and `FetchRemoteCatalog()` returns no products. - ---- - -## General Best Practices - -- **Always let the skill scan first.** The pre-check (`pre-check.md`) detects third-party packages, native billing code, and existing Unity IAP versions before routing. Skipping it leads to incompatible changes. -- **Read the migration report before approving any code changes.** Every conversion path produces a report covering what will change, what blockers were found, and what manual steps remain. Review it before proceeding. -- **Never confirm a purchase before saving the reward.** An unconfirmed purchase re-delivers safely. A confirmed-but-unsaved one is gone permanently. -- **Subscribe to all failure events.** `OnProductsFetchFailed`, `OnPurchasesFetchFailed`, `OnStoreDisconnected` — not subscribing generates runtime warnings and leaves failures silently unhandled. -- **Subscribe to `OnPurchaseDeferred`.** Ask-to-Buy (iOS) and Google Play deferred purchases fire this event. Not subscribing silently drops them. -- **Use `#if USE_UNITY_IAP_V5` guards** for all migration work. The rollback path (remove the define from Player Settings) must work cleanly before shipping. -- **Test with real sandbox accounts on real devices.** Editor sandbox and emulators do not reproduce all edge cases — particularly pending purchases, deferred flows, and restore behavior. diff --git a/skills/implement-in-app-purchases/SKILL.md b/skills/implement-in-app-purchases/SKILL.md index 1a25b8f..b39a52c 100644 --- a/skills/implement-in-app-purchases/SKILL.md +++ b/skills/implement-in-app-purchases/SKILL.md @@ -1,6 +1,6 @@ --- name: implement-in-app-purchases -description: Implements, configures, and debugs Unity In-App Purchases, including subscriptions, receipt validation, and direct-to-customer payments via Stripe or Coda. Use when asked about real-money purchases or microtransactions, or migrating from native store billing, RevenueCat, or Adapty. +description: Implements, configures, and debugs Unity In-App Purchases, including subscriptions, receipt validation, and direct-to-customer payments via Stripe or Coda. Use when asked about real-money purchases or microtransactions, or migrating from native store billing, RevenueCat, Adapty, Essential Kit, or UniPay. --- # Unity In-App Purchasing diff --git a/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md b/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md index 6c93f12..5e2e93c 100644 --- a/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +++ b/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md @@ -304,14 +304,7 @@ If the project already has a `StoreController` for Apple App Store or Google Pla Before initiating purchase, use `GetEligiblePaymentProviders()` to confirm that at least one payment provider is available for the current user and region. Call this after `Connect()` and `FetchProducts()` succeed: ```csharp -var svc = store.PaymentProviderStoreExtendedService; -if (svc == null) -{ - // No payment provider service — hide purchase UI or show an error - return; -} - -var eligible = await svc.GetEligiblePaymentProviders(); +var eligible = await store.PaymentProviderStoreExtendedService?.GetEligiblePaymentProviders(); if (eligible == null || eligible.Providers.Count == 0) { // No payment providers available — hide purchase UI or show an error @@ -346,8 +339,7 @@ Then initiate purchase with the `catalogListingId`: ```csharp public async void Buy(string catalogListingId) { - var svc = store.PaymentProviderStoreExtendedService; - var eligibility = svc != null ? await svc.GetEligiblePaymentProviders() : null; + var eligibility = await store.PaymentProviderStoreExtendedService?.GetEligiblePaymentProviders(); if (eligibility?.Providers.Count > 0) { // Show the Purchase Options UI — lets the player pick native, D2C, or webshop @@ -495,14 +487,8 @@ Returns `null` when the product wasn't fetched through the PaymentProvider store Skip the picker entirely and open the webshop for a specific listing (or the generic Unity webshop when `catalogListingId` is null): ```csharp -var svc = store.PaymentProvidersExtendedPurchaseService; -if (svc == null) -{ - // No payment provider purchase service — surface an error or hide the entry point - return; -} - -await svc.RedirectToWebshop("coins_100_offer_usd"); +await store.PaymentProvidersExtendedPurchaseService? + .RedirectToWebshop("coins_100_offer_usd"); ``` The SDK fetches the webshop URL, runs the registered compliance callback (`SetComplianceCheck`), and opens the URL on approval. Network failures propagate as exceptions on the returned `Task`; compliance rejection routes through the standard `OnPurchaseFailed` path. diff --git a/skills/migrate-birp-to-urp/SECURITY.md b/skills/migrate-birp-to-urp/SECURITY.md index 90476cc..8449644 100644 --- a/skills/migrate-birp-to-urp/SECURITY.md +++ b/skills/migrate-birp-to-urp/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25. diff --git a/skills/optimize-audio/SECURITY.md b/skills/optimize-audio/SECURITY.md index a037869..dcbaec8 100644 --- a/skills/optimize-audio/SECURITY.md +++ b/skills/optimize-audio/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25. diff --git a/skills/optimize-web/SECURITY.md b/skills/optimize-web/SECURITY.md index 01301db..f1d95e8 100644 --- a/skills/optimize-web/SECURITY.md +++ b/skills/optimize-web/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25. diff --git a/skills/sprite-editor/SECURITY.md b/skills/sprite-editor/SECURITY.md index 405ce17..77d4091 100644 --- a/skills/sprite-editor/SECURITY.md +++ b/skills/sprite-editor/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25. diff --git a/skills/ui-ugui/SKILL.md b/skills/ui-ugui/SKILL.md index 1a8869a..9c5e114 100644 --- a/skills/ui-ugui/SKILL.md +++ b/skills/ui-ugui/SKILL.md @@ -6,7 +6,7 @@ description: Understands, edits, and generates Unity uGUI Canvas hierarchies, Re Understand existing Unity uGUI, make targeted edits, and generate new Canvas-based hierarchies. When working with ScrollRect/ScrollView, read the reference file: -- `references/scrollview-setup.md` — Required hierarchy, setup rules, and common failures +- [references/scrollview-setup.md](references/scrollview-setup.md) — Required hierarchy, setup rules, and common failures ## Scope diff --git a/skills/ui-uitk/SKILL.md b/skills/ui-uitk/SKILL.md index 9e61144..92c3a5e 100644 --- a/skills/ui-uitk/SKILL.md +++ b/skills/ui-uitk/SKILL.md @@ -8,13 +8,13 @@ Understand existing Unity UI Toolkit code, make targeted edits, generate new UXM ## References Read these as needed: -- `references/uss-guide.md` — USS patterns and examples -- `references/svg-icons.md` — SVG icon generation (only when generating icons) -- `references/common-issues.md` — Common mistakes to avoid -- `references/ui-runtime-binding.md` — Patterns and guide to bind data to UI at runtime (only when requested or when bindings are involved) -- `references/painter2d.md` — Painter2D API for custom visuals: gradients, shapes, arcs, procedural drawing (read this whenever gradients, custom shapes, progress rings, procedural drawing, or any visual beyond what USS can express is needed) -- `references/pointermanipulator-guide.md` — Patterns and guide to create and use Manipulators (only when requested or when manipulators are involved). This helps with setting up drag and drop features or simple event handling for a Visual Element. -- `references/custom-elements.md` — Custom UI Element patterns and guide to create reusable components with UXML, USS, and C#. This helps with creating complex UI components for reuse across the project. +- [references/uss-guide.md](references/uss-guide.md) — USS patterns and examples +- [references/svg-icons.md](references/svg-icons.md) — SVG icon generation (only when generating icons) +- [references/common-issues.md](references/common-issues.md) — Common mistakes to avoid +- [references/ui-runtime-binding.md](references/ui-runtime-binding.md) — Patterns and guide to bind data to UI at runtime (only when requested or when bindings are involved) +- [references/painter2d.md](references/painter2d.md) — Painter2D API for custom visuals: gradients, shapes, arcs, procedural drawing (read this whenever gradients, custom shapes, progress rings, procedural drawing, or any visual beyond what USS can express is needed) +- [references/pointermanipulator-guide.md](references/pointermanipulator-guide.md) — Patterns and guide to create and use Manipulators (only when requested or when manipulators are involved). This helps with setting up drag and drop features or simple event handling for a Visual Element. +- [references/custom-elements.md](references/custom-elements.md) — Custom UI Element patterns and guide to create reusable components with UXML, USS, and C#. This helps with creating complex UI components for reuse across the project. Paths are relative to this skill's folder — read `references/uss-guide.md` directly. diff --git a/skills/urp-postprocessing/SECURITY.md b/skills/urp-postprocessing/SECURITY.md index 272605a..71fa545 100644 --- a/skills/urp-postprocessing/SECURITY.md +++ b/skills/urp-postprocessing/SECURITY.md @@ -14,7 +14,3 @@ This skill has the agent run C# inside the user's open Unity Editor through `uni - **Capability gate.** `eval` is only available when the project's Pipeline package provides it. - **No remote code.** The agent runs C# it writes from this skill's own recipes. Nothing downloaded from outside is executed. - **Named commands first.** When a dedicated `unity command` covers a step, the skill uses it instead of `eval`. - -## Background - -The `unity command eval` wording was added when this skill moved from the AI Assistant's built-in `RunCommand` tool to the `unity` CLI, not by the skill's original author. Ziyi Zhang, who made that change, accepted this risk on 2026-09-25.