nodecanvas-cli 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.claude-plugin/plugin.json +1 -1
  2. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/CHANGELOG.md +47 -0
  3. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/PKG-INFO +6 -4
  4. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/README.md +5 -3
  5. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/pyproject.toml +1 -1
  6. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/SKILL.md +44 -10
  7. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/references/ops.md +8 -3
  8. nodecanvas_cli-0.3.0/src/nodecanvas_cli/__init__.py +1 -0
  9. nodecanvas_cli-0.3.0/src/nodecanvas_cli/assetrefs.py +181 -0
  10. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/assets.py +19 -66
  11. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/bindings.py +24 -60
  12. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/build.py +38 -27
  13. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/builtin.py +31 -5
  14. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/cli.py +3 -3
  15. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/csharp.py +19 -0
  16. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/discover.py +57 -27
  17. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/lint.py +174 -46
  18. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/mcp_server.py +21 -15
  19. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/model.py +2 -0
  20. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/ops.py +124 -3
  21. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/owners.py +26 -24
  22. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/refactor.py +95 -17
  23. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/refs.py +13 -2
  24. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/render.py +2 -2
  25. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/scaffold.py +6 -1
  26. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/schema.py +168 -22
  27. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/spec.py +10 -9
  28. nodecanvas_cli-0.3.0/src/nodecanvas_cli/typedwalk.py +337 -0
  29. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/typenames.py +82 -4
  30. nodecanvas_cli-0.3.0/src/nodecanvas_cli/typerel.py +281 -0
  31. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/verify.py +100 -20
  32. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/views.py +12 -9
  33. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/workspace.py +33 -0
  34. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs +2 -0
  35. nodecanvas_cli-0.3.0/tests/test_blackboard_ops.py +178 -0
  36. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_lint.py +76 -0
  37. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_mcp.py +4 -0
  38. nodecanvas_cli-0.3.0/tests/test_object_refs.py +177 -0
  39. nodecanvas_cli-0.3.0/tests/test_owners.py +252 -0
  40. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_refactor.py +58 -0
  41. nodecanvas_cli-0.3.0/tests/test_type_contracts.py +308 -0
  42. nodecanvas_cli-0.3.0/tests/test_verify.py +239 -0
  43. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/uv.lock +1 -1
  44. nodecanvas_cli-0.2.0/src/nodecanvas_cli/__init__.py +0 -1
  45. nodecanvas_cli-0.2.0/tests/test_owners.py +0 -116
  46. nodecanvas_cli-0.2.0/tests/test_verify.py +0 -131
  47. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.claude-plugin/marketplace.json +0 -0
  48. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.gitattributes +0 -0
  49. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  50. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.github/workflows/release.yml +0 -0
  51. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.gitignore +0 -0
  52. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/LICENSE +0 -0
  53. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/bin/nodecanvas-cli +0 -0
  54. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/scripts/roundtrip_check.py +0 -0
  55. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/scripts/nodecanvas-cli +0 -0
  56. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/__main__.py +0 -0
  57. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/config.py +0 -0
  58. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/container.py +0 -0
  59. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/dialogue.py +0 -0
  60. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/explain.py +0 -0
  61. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/fsjson.py +0 -0
  62. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/layout.py +0 -0
  63. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/notation.txt +0 -0
  64. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/project.py +0 -0
  65. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/unity_verify.cs.txt +0 -0
  66. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/yamlscalar.py +0 -0
  67. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/conftest.py +0 -0
  68. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/generate.py +0 -0
  69. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Blackboards/Shared.asset +0 -0
  70. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Blackboards/Shared.asset.meta +0 -0
  71. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/Bot.cs +0 -0
  72. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/Bot.cs.meta +0 -0
  73. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/GuardOwner.cs +0 -0
  74. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/GuardOwner.cs.meta +0 -0
  75. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_BT.asset +0 -0
  76. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_BT.asset.meta +0 -0
  77. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset +0 -0
  78. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset.meta +0 -0
  79. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Broken_BT.asset +0 -0
  80. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Broken_BT.asset.meta +0 -0
  81. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset +0 -0
  82. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset.meta +0 -0
  83. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Sub_BT.asset +0 -0
  84. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Sub_BT.asset.meta +0 -0
  85. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Talk_DT.asset +0 -0
  86. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Talk_DT.asset.meta +0 -0
  87. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Guard.prefab +0 -0
  88. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Guard.prefab.meta +0 -0
  89. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Sentry.prefab +0 -0
  90. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Sentry.prefab.meta +0 -0
  91. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Scenes/World.unity +0 -0
  92. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Scenes/World.unity.meta +0 -0
  93. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs.meta +0 -0
  94. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/ProjectSettings/ProjectVersion.txt +0 -0
  95. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Bot_BT.txt +0 -0
  96. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Bot_FSM.txt +0 -0
  97. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Broken_BT.txt +0 -0
  98. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Patrol_BT.txt +0 -0
  99. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Sentry.txt +0 -0
  100. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Sub_BT.txt +0 -0
  101. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Talk_DT.txt +0 -0
  102. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_apply.py +0 -0
  103. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_assets.py +0 -0
  104. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_codec.py +0 -0
  105. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_csharp.py +0 -0
  106. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_dialogue.py +0 -0
  107. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_errors.py +0 -0
  108. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_explain.py +0 -0
  109. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_launcher.py +0 -0
  110. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_read.py +0 -0
  111. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_scaffold.py +0 -0
  112. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_spec.py +0 -0
  113. {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_typenames.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodecanvas-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read, search, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects through the nodecanvas-cli command, without opening Unity.",
5
5
  "author": {
6
6
  "name": "Aleksandr Esipov"
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-11-06
4
+
5
+ - `refactor rename-member`: several renames in one run — `Type.Old=New` pairs (`Type.Old(P1,P2)=New` picks an
6
+ overload) and/or `--file` with one pair per line. One combined diff and lint pass, a per-pair summary,
7
+ one save; if any pair fails nothing is written. Chains and swaps are rejected. The MCP
8
+ `refactor_rename_member` tool takes `renames`.
9
+ - `lint --changed`: lint only graphs in files changed since `HEAD` (plus untracked), like
10
+ `verify --unity --changed`; the MCP `lint` tool takes `changed`. `lint` and `verify` reject `--changed`
11
+ together with graph names.
12
+ - `lint`: a type saved under a namespace that no longer exists, but found by its short name, is now a
13
+ `stale-type-name` warning (with "ambiguous" when several types share the name) instead of a
14
+ `missing-type` / `missing-var-type` error. NodeCanvas resolves such names by short name on load, so
15
+ `verify --unity` and `lint` used to disagree. `show`/`vars` render these wrappers as `(recovered)`.
16
+ - `lint`: the summary line counts issues hidden by `--min` (`40 info hidden, --min info to show`).
17
+ - `verify --unity`: notes how many Missing wrappers in the file NodeCanvas restored on load.
18
+ - Type names typed by a person (`new --var`, `var-add`, `SetVariable<T>`, `call Type.Member`, `scaffold --agent`,
19
+ `members`) never resolve by guess. A short name shared by several types is an error listing them, partial
20
+ qualifiers work (`Arena.Pawn`), a wrong qualifier is no longer ignored, and an unknown short name is an error with
21
+ suggestions instead of an invented `Name, Assembly-CSharp`. `refactor rename-member Pawn.X=Y` refuses when
22
+ graphs call two types of that name, where it used to rename both. **Possibly breaking:** types outside the project's
23
+ sources now need their full name (`UnityEngine.AI.NavMeshAgent`).
24
+ - New type model, following `ParadoxNotion.TypeConverter` and comparing full names. `lint`: new
25
+ `param-type-mismatch` (task and node parameters against bound variables, with generic types substituted);
26
+ `mapping-type-mismatch` checks each direction and can now be an error; `arg-type-mismatch` too. Two unrelated
27
+ components with the same short name or types with no conversion are errors, so `apply`, `copy` and `new` refuse
28
+ to introduce them without `--force`.
29
+ - `verify --unity` includes static lint errors and type-contract warnings: Unity reports neither, because such
30
+ types "convert" to null through `GetComponent`.
31
+ - `verify --unity` waits for an Editor that is compiling or reloading scripts instead of failing with "no Editor
32
+ reachable" right after `unity recompile`, repeats the check if a reload interrupts it, and reports Safe Mode with
33
+ its compile errors and an open modal dialog as such.
34
+ - Messages, `vars`, `show` and `apply` diffs print full names for types whose short name is not unique.
35
+ - `_objectReferences`: entries nothing uses are removed on every save, so `subgraph-set` no longer leaves the old
36
+ GUID behind. New `stale-object-ref` lint warning for files that already have such entries. Entries are never
37
+ touched while a field of unknown type might be using them.
38
+ - `subgraph-set` moves mappings to the new graph's variables of the same name and type and removes the rest,
39
+ instead of leaving stale ones.
40
+ - Fixed: `extract`, `inline`, `copy --create-vars` and `new --from` copied object values of variables with indices
41
+ that meant another object, or nothing, in the destination graph.
42
+ - `owners` / `refs`: references to a graph from any field of any asset (`referenced by:
43
+ Kai_BotData.asset (BotData).fsm`); such graphs are no longer reported as unassigned. Also finds Addressables
44
+ references and object overrides in `m_Modifications`.
45
+ - `types` explains fields: `[Tooltip]`, `[Name]`, required / blackboard-only marks, also in `--json` (`fields`).
46
+ The built-in declarations carry short explanations of the main nodes, and `types` lists them in projects
47
+ without NodeCanvas sources.
48
+ - `apply`: `var-add` takes `after`, `before` or `index`; new `var-move`; `var-set` takes `type`.
49
+
3
50
  ## 0.2.0 — 2026-10-16
4
51
 
5
52
  - Claude Code plugin: `bin/nodecanvas-cli` puts the command on the agent's PATH (Claude Code 2.1.91+),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nodecanvas-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Read, query, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects from the command line
5
5
  Project-URL: Homepage, https://github.com/Primeer/nodecanvas-cli
6
6
  Project-URL: Issues, https://github.com/Primeer/nodecanvas-cli/issues
@@ -80,7 +80,8 @@ vars: Self:Bot(pub) Target:Unit Alert:bool Cool time:float Speed:float
80
80
  transitions in, dialogue choices), the same for every call of a member or write of a variable, and
81
81
  graph statistics.
82
82
  - **owners / blackboards** — which prefabs and scenes run a graph (GraphOwner components, exposed
83
- variable overrides, owner blackboards), graphs assigned nowhere, and Blackboard / GlobalBlackboard /
83
+ variable overrides, owner blackboards), which other assets point at it (a field of a ScriptableObject, say),
84
+ graphs nothing refers to, and Blackboard / GlobalBlackboard /
84
85
  AssetBlackboard contents; lint checks global variables and owner overrides against them.
85
86
  - **types / members** — discover task and node types with their fields, defaults and enum members, and the
86
87
  properties/methods reflection tasks can call.
@@ -154,12 +155,13 @@ nodecanvas-cli why <graph> <node> [--expand] # conditions on the way from
154
155
  nodecanvas-cli paths [graph] --to <Type.Member | $Var> [--reads]
155
156
  nodecanvas-cli stats [graph] [--json]
156
157
  nodecanvas-cli scaffold action|condition|decorator|composite <Name> --dir Assets/... [--field "x:BBParameter<float>=1"]
157
- nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs
158
+ nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs, other assets
158
159
  nodecanvas-cli blackboards [--global]
159
- nodecanvas-cli lint [graph...] [--min info|warning|error] [--json]
160
+ nodecanvas-cli lint [graph... | --changed] [--min info|warning|error] [--json]
160
161
  nodecanvas-cli types [query] [--role action|condition|bt|composite|decorator|fsm|dt]
161
162
  nodecanvas-cli members <Type> [filter]
162
163
  nodecanvas-cli refactor rename-member|rename-type|retarget ... [--dry-run]
164
+ nodecanvas-cli refactor rename-member Type.Old=New [Type.Old(P1,P2)=New ...] [--file pairs.txt]
163
165
  nodecanvas-cli new <path.asset> --kind bt|fsm|dt [--var "Name:type[=value][:pub]"]
164
166
  nodecanvas-cli extract <graph> <node> <new.asset> | inline <graph> <node> | copy <src> <node> <dst> <parent>
165
167
  nodecanvas-cli apply <graph> <ops.json | -> [--dry-run] [--force]
@@ -49,7 +49,8 @@ vars: Self:Bot(pub) Target:Unit Alert:bool Cool time:float Speed:float
49
49
  transitions in, dialogue choices), the same for every call of a member or write of a variable, and
50
50
  graph statistics.
51
51
  - **owners / blackboards** — which prefabs and scenes run a graph (GraphOwner components, exposed
52
- variable overrides, owner blackboards), graphs assigned nowhere, and Blackboard / GlobalBlackboard /
52
+ variable overrides, owner blackboards), which other assets point at it (a field of a ScriptableObject, say),
53
+ graphs nothing refers to, and Blackboard / GlobalBlackboard /
53
54
  AssetBlackboard contents; lint checks global variables and owner overrides against them.
54
55
  - **types / members** — discover task and node types with their fields, defaults and enum members, and the
55
56
  properties/methods reflection tasks can call.
@@ -123,12 +124,13 @@ nodecanvas-cli why <graph> <node> [--expand] # conditions on the way from
123
124
  nodecanvas-cli paths [graph] --to <Type.Member | $Var> [--reads]
124
125
  nodecanvas-cli stats [graph] [--json]
125
126
  nodecanvas-cli scaffold action|condition|decorator|composite <Name> --dir Assets/... [--field "x:BBParameter<float>=1"]
126
- nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs
127
+ nodecanvas-cli owners [<graph> | --all] # GraphOwners in prefabs/scenes, parent graphs, other assets
127
128
  nodecanvas-cli blackboards [--global]
128
- nodecanvas-cli lint [graph...] [--min info|warning|error] [--json]
129
+ nodecanvas-cli lint [graph... | --changed] [--min info|warning|error] [--json]
129
130
  nodecanvas-cli types [query] [--role action|condition|bt|composite|decorator|fsm|dt]
130
131
  nodecanvas-cli members <Type> [filter]
131
132
  nodecanvas-cli refactor rename-member|rename-type|retarget ... [--dry-run]
133
+ nodecanvas-cli refactor rename-member Type.Old=New [Type.Old(P1,P2)=New ...] [--file pairs.txt]
132
134
  nodecanvas-cli new <path.asset> --kind bt|fsm|dt [--var "Name:type[=value][:pub]"]
133
135
  nodecanvas-cli extract <graph> <node> <new.asset> | inline <graph> <node> | copy <src> <node> <dst> <parent>
134
136
  nodecanvas-cli apply <graph> <ops.json | -> [--dry-run] [--force]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nodecanvas-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Read, query, lint and edit NodeCanvas graphs (Behaviour Trees, FSMs, Dialogue Trees) in Unity projects from the command line"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -89,8 +89,10 @@ their defaults are shown with their real meaning (taken from the project's C# so
89
89
  - `nodecanvas-cli owners <graph>` — prefabs/scenes whose GraphOwner (BehaviourTreeOwner, FSMOwner, project
90
90
  subclasses) runs the graph, with the owner's Blackboard (and its parent AssetBlackboard) and the
91
91
  exposed-variable overrides (`$Var = value`); also parent graphs using it as a subgraph, and PrefabInstances
92
- overriding `_graph` (listed, not parsed further). `owners --all` — table of every graph; graphs
93
- assigned nowhere in assets are flagged (they may still be assigned from code).
92
+ overriding `_graph` (listed, not parsed further), and any other asset whose field points at the graph
93
+ (`referenced by: …Kai_BotData.asset (BotData).fsm`: a ScriptableObject, a component in a prefab or scene, an
94
+ Addressables reference). `owners --all` — table of every graph; graphs nothing in assets refers to are flagged
95
+ (they may still be loaded from code). `refs <graph>` lists these references too.
94
96
  - `nodecanvas-cli blackboards [--global]` — Blackboard / GlobalBlackboard components and AssetBlackboards
95
97
  with their variables.
96
98
  - `vars` and `show` mark variables that are not on the graph's own blackboard with their source:
@@ -107,6 +109,10 @@ all commands work on every graph at once, print a compact diff per graph and sup
107
109
  tell which one moved (calls of the others are kept and noted), or pick it with `--params Pawn,Vector3`
108
110
  (`--params ''` for no parameters); `--all-overloads` renames every overload. When graphs call several
109
111
  overloads and nothing tells which one is meant, the command stops with an error.
112
+ Several members at once — one diff, one save, nothing written if any pair fails:
113
+ `rename-member Bot.Attack=Strike Bot.speed=moveSpeed "Bot.MoveTo(Unit,Vector3)=Go"` (the `(…)` suffix picks
114
+ the overload for that pair), or `--file renames.txt` with one `Type.Old=New` per line (`-` for stdin).
115
+ Chains and swaps (`A=B B=A`) need two runs.
110
116
  - `nodecanvas-cli refactor rename-type Old.Namespace.Type New.Namespace.Type [--assembly Asm]` — namespace,
111
117
  name or nesting changes (also fixes "missing type" wrappers left by an earlier rename).
112
118
  - `nodecanvas-cli refactor retarget BotController.DistanceTo --to Booster [--from Pawn]` — move calls to another
@@ -158,14 +164,31 @@ drops the TODO comments (for projects that do not want comments).
158
164
 
159
165
  ## Checking graphs
160
166
 
161
- `nodecanvas-cli lint [graph...] [--min info]` reports missing task/variable types, variables that are not on
167
+ `nodecanvas-cli lint [graph... | --changed] [--min info]` (`--changed`: only graphs in files changed since
168
+ `HEAD`, plus untracked ones) reports missing task/variable types, variables that are not on
162
169
  the blackboard, reflection bindings to members that no longer exist, unassigned or missing subgraphs,
163
- stale subgraph variable mappings, arguments whose stored or variable type does not fit the bound
164
- overload (`arg-type-mismatch`, with a `fix:` op when another overload fits), subgraph mappings between
165
- variables of different types, too many children for a node type, unreachable nodes, and graph assets whose
170
+ stale subgraph variable mappings, type contracts between variables and what they are bound to (below),
171
+ object references nothing uses (`stale-object-ref`), too many children for a node type, unreachable nodes, and graph assets whose
166
172
  `m_Name` differs from the file name (`name-mismatch`, warning — e.g. after renaming the file outside Unity). It exits with 1 when there are errors. Run it
167
173
  after C# refactors that touch types used by graphs.
168
174
 
175
+ Type contracts: `param-type-mismatch` (a task or node parameter, generic ones included: `SetVariable<A.Pawn>` bound
176
+ to a variable of `B.Pawn`), `mapping-type-mismatch` (a subgraph mapping, in each direction it flows) and
177
+ `arg-type-mismatch` (a reflection argument, with a `fix:` op when another overload fits) follow NodeCanvas's own
178
+ conversions, comparing full type names:
179
+
180
+ | Case | Severity |
181
+ |---|---|
182
+ | Same type, subclass, `object`, number to a wider number, anything to `string` | none |
183
+ | Through the GameObject: `GameObject` ↔ component, a Unity component ↔ a project one, component → `Vector3` | info |
184
+ | Other conversions (`bool` ↔ `float`, `float` → `int`, `Vector3` → `Vector2`) | warning |
185
+ | Two unrelated project components with different short names: `GetComponent` finds something only if the GameObject has both | warning |
186
+ | Two unrelated project components with the same short name (two `Pawn`s): almost certainly the wrong type | error |
187
+ | No conversion at all | error |
188
+ | A type outside the sources | not reported (info for a mapping) |
189
+
190
+ Unity reports none of these, since such types do convert (to null): the static checks are what catches them.
191
+
169
192
  Owners and blackboards: `missing-global-var` (warning) — `$Name/Var` has no GlobalBlackboard
170
193
  `Name` with that variable; `exposed-override-type` / `exposed-override-stale` (warning) — an owner
171
194
  overrides an exposed variable with another type, or one the graph no longer has; `owner-missing-graph`
@@ -185,7 +208,14 @@ editor's minimum node size; `off-grid` (info).
185
208
 
186
209
  `recoverable-missing` (info) means NodeCanvas once saved a task or variable as "missing" while its type
187
210
  did not compile; the type exists again, Unity restores it on load and the wrapper disappears on the
188
- next save in the editor. `show` renders such tasks with `(recovered)`. It is not an error.
211
+ next save in the editor. `show` renders such tasks with `(recovered)`. It is not an error. Info is
212
+ hidden by default: the summary line says how many (`56 info hidden, --min info to show`).
213
+
214
+ `stale-type-name` (warning): a saved type name (or a generic argument of it) no longer exists under its
215
+ full name, but a type with the same short name does — usually its namespace changed. NodeCanvas falls
216
+ back to the short name on load, so Unity loads the graph and `verify --unity` reports it as fine;
217
+ `verify --unity --resave` stores the current name. With several types of that name the message says
218
+ "ambiguous": NodeCanvas then takes whichever it meets first.
189
219
 
190
220
  ## Checking graphs in the Unity Editor
191
221
 
@@ -193,7 +223,10 @@ next save in the editor. `show` renders such tasks with `(recovered)`. It is not
193
223
  open Unity Editor to re-import each graph, deserialize and validate it, and reports what NodeCanvas sees:
194
224
  node/task errors shown red in the graph editor (missing tasks, missing reflected members), Missing
195
225
  wrappers left after loading, overloads resolved by name fallback, node/variable counts that differ from
196
- the file, and console errors logged meanwhile. It waits for script compilation first. Exit code 0 — all
226
+ the file, and console errors logged meanwhile. Every static lint error and type-contract warning for the
227
+ graph is added (`[mapping-type-mismatch] …`), because Unity cannot see those: an error makes the graph `FAIL`, a
228
+ warning prints under `ok`. It waits for script compilation first, and for an Editor that is reloading scripts
229
+ (right after `unity recompile`, say); a project in Safe Mode is reported with its compile errors. Exit code 0 — all
197
230
  fine, 1 — problems, 2 — no Editor reachable (it needs the `unity` CLI and the project's
198
231
  `com.unity.pipeline` package; `unity pipeline install` adds it). Use it after edits when an Editor is
199
232
  open, instead of asking the user to open the graph. Graphs bound in scenes are checked only when the
@@ -205,7 +238,7 @@ wrappers); it changes files, so use it only when asked.
205
238
  1. `nodecanvas-cli show <graph>` — note the `#ids` you will touch (ids are positions; they change after every edit).
206
239
  2. To learn how to write a node, print a similar existing one with `nodecanvas-cli node <graph> <id>` and
207
240
  adapt it. For a task or node type you have not seen yet: `nodecanvas-cli types <Name>` (fields, defaults,
208
- enum values) or `nodecanvas-cli types --role action|condition|bt|composite|decorator|fsm|dt`.
241
+ enum values, and what each field means when the source has a `[Tooltip]`) or `nodecanvas-cli types --role action|condition|bt|composite|decorator|fsm|dt`.
209
242
  For reflection tasks: `nodecanvas-cli members <Type> [filter]` lists callable properties/methods.
210
243
  3. Write a JSON array of operations and run `nodecanvas-cli apply <graph> ops.json --dry-run` (or `-` for stdin).
211
244
  Read the printed before/after diff (blackboard changes follow it as `vars:` lines: `+`/`-` added or removed,
@@ -213,7 +246,8 @@ wrappers); it changes files, so use it only when asked.
213
246
  placed, moved by the edit, and which existing subtrees were shifted (and by how much) to make room.
214
247
  Add `--positions` to see coordinates in the diff. Then run again without `--dry-run`.
215
248
  4. `nodecanvas-cli show <graph>` again to get the new ids before the next batch.
216
- 5. If a Unity Editor is open on the project, `nodecanvas-cli verify --unity --changed` confirms Unity loads the result.
249
+ 5. After the last batch, `nodecanvas-cli lint --changed` checks every graph you touched; if a Unity Editor is open
250
+ on the project, `nodecanvas-cli verify --unity --changed` confirms Unity loads the result.
217
251
 
218
252
  A batch is atomic: if any operation fails, or the result has new lint errors, nothing is written (use
219
253
  `--force` only when the error is expected). Connection limits (e.g. a BinarySelector's two children)
@@ -102,16 +102,21 @@ syntax as the `map` operation; `nodecanvas-cli node` prints them that way.
102
102
 
103
103
  | Op | Fields | Notes |
104
104
  |---|---|---|
105
- | `var-add` | `name`, `type`, `value`?, `public`? | `type` is C# (`float`, `bool`, `Vector3`, `Pawn`, `List<Pawn>`). |
106
- | `var-set` | `name`, `value`?, `public`? | Changes the stored value / exposure. |
105
+ | `var-add` | `name`, `type`, `value`?, `public`?, `after`?/`before`?/`index`? | `type` is C# (`float`, `bool`, `Vector3`, `Pawn`, `List<Pawn>`). Without a position the variable goes last. `after`/`before` name a variable or a section (`"after": "Parameters"`); `index` counts sections too. |
106
+ | `var-set` | `name`, `value`?, `public`?, `type`? | Changes the stored value / exposure / type. `type` keeps the id, so references stay; the old value is converted between primitive types, kept between object types, otherwise dropped (a note says so). Bindings the new type breaks are refused by lint. |
107
+ | `var-move` | `name`, `after`/`before`/`index` | Moves a variable, for example from `Local variables` into `Parameters`. |
107
108
  | `var-rename` | `name`, `to` | Updates every reference in this graph. Subgraph mappings in *other* graphs keep working (they use ids). |
108
109
  | `var-remove` | `name`, `force`? | Refuses while the variable is used, unless `force`. |
109
110
 
111
+ Type names here and in `$type`, `call` and the like must identify one type. A short name shared by several types
112
+ (two `Pawn`s; a project `Transform` and Unity's) is an error listing them: give a full or partial name
113
+ (`Arena.Pawn`). A type outside the project's sources needs its full name (`TMPro.TMP_Text`).
114
+
110
115
  ## Subgraphs
111
116
 
112
117
  | Op | Fields | Notes |
113
118
  |---|---|---|
114
- | `subgraph-set` | `node`, `graph`, `map`? | Points a SubTree / nested state at another graph asset. |
119
+ | `subgraph-set` | `node`, `graph`, `map`? | Points a SubTree / nested state at another graph asset. Without `map`, existing mappings move to the new graph's variables of the same name and type; the rest are removed, with a note. |
115
120
  | `map` | `node`, `map` | Replaces the variable mapping: `{"SubVar": "<-$Parent"}` passes a value in, `"->$Parent"` reads it back after the subgraph runs, `"<->$Parent"` both; a literal passes a constant in. |
116
121
  | `map` | `node`, `"auto": true` | Adds mappings for the subgraph's public variables that are not mapped yet, to parent variables with the same name and type; direction from how the subgraph uses each (read → `<-`, written → `->`, both → `<->`). Prints what was mapped and skipped. |
117
122
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,181 @@
1
+ """Who references a graph asset from outside the graph system: any field of any ScriptableObject, prefab or scene.
2
+
3
+ A project may hand graphs out through its own data (``BotData.fsm``), so a graph with no GraphOwner and no parent
4
+ graph is not necessarily dead. Unity keeps such a reference as ``{fileID: 11400000, guid: …, type: 2}`` in the YAML
5
+ of the referencing object (``m_AssetGUID`` for an Addressables reference), so a text scan finds them all.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import re
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Dict, List
15
+
16
+ from .container import ObjectRef, _documents, _value_of
17
+ from .project import CONTAINER_SUFFIXES, Cache
18
+
19
+ _GUID_RE = re.compile(rb"[Gg][Uu][Ii][Dd]: ([0-9a-f]{32})")
20
+ _FILE_ID_RE = re.compile(r"fileID: (-?\d+)")
21
+ # Object lists of graph containers: an entry there is a use only if the graph really uses it.
22
+ GRAPH_REF_LISTS = ("_objectReferences", "_boundGraphObjectReferences")
23
+ _CACHE_KEY = "\0graph guids" # what the entries were computed for: another set of graphs means another scan
24
+
25
+
26
+ @dataclass
27
+ class AssetRef:
28
+ path: str
29
+ obj: str # the referencing object: "Guard (BotBrain)", "(BotData)", "PrefabInstance"
30
+ field: str # dotted path of the field
31
+
32
+ @property
33
+ def label(self) -> str:
34
+ return f"{self.path} {self.obj}.{self.field}" if self.obj else f"{self.path} {self.field}"
35
+
36
+
37
+ def yaml_path(lines: List[str], i: int, start: int) -> str:
38
+ """Dotted field path of line ``i`` inside the document starting at ``start``, from Unity's YAML indentation
39
+ (list items sit at the indentation of their key)."""
40
+
41
+ def split(text: str):
42
+ item = text.startswith("- ")
43
+ body = text[2:] if item else text
44
+ key = body.split(":", 1)[0].strip() if ":" in body and not body.startswith("{") else None
45
+ return item, key
46
+
47
+ text = lines[i].lstrip(" ")
48
+ col = len(lines[i]) - len(text)
49
+ in_item, key = split(text)
50
+ parts = [key] if key else []
51
+ for j in range(i - 1, start, -1):
52
+ text = lines[j].lstrip(" ")
53
+ ind = len(lines[j]) - len(text)
54
+ if not text or ind == 0:
55
+ continue
56
+ item, key = split(text)
57
+ if in_item: # the key of this list is at the same indentation
58
+ if ind <= col and not item and key:
59
+ parts.append(key)
60
+ col, in_item = ind, False
61
+ continue
62
+ if item and ind + 2 == col: # a sibling on the list item's own line
63
+ col, in_item = ind, True
64
+ elif ind < col and key:
65
+ parts.append(key)
66
+ col, in_item = ind, item
67
+ return ".".join(reversed(parts))
68
+
69
+
70
+ def _object_of(ws, lines: List[str], doc, go_names: Dict[str, str]) -> str:
71
+ start, end, cls, _ = doc
72
+ if cls == "1001":
73
+ return "PrefabInstance"
74
+ script = _value_of(lines, start, end, "m_Script") or ""
75
+ name = ""
76
+ if "guid" in script:
77
+ path = ws.ref_path(ObjectRef.parse(script))
78
+ name = Path(path).stem if path else ""
79
+ if not name:
80
+ name = (_value_of(lines, start, end, "m_EditorClassIdentifier") or "").split("::")[-1].split(".")[-1]
81
+ m = _FILE_ID_RE.search(_value_of(lines, start, end, "m_GameObject") or "")
82
+ go = go_names.get(m.group(1), "") if m and m.group(1) != "0" else ""
83
+ return f"{go} ({name})" if go and name else go or (f"({name})" if name else "")
84
+
85
+
86
+ def _scan_file(ws, rel: str, text: str, guids: set) -> List[list]:
87
+ lines = text.split("\n")
88
+ docs = _documents(lines)
89
+ go_names = {fid: _value_of(lines, s, e, "m_Name") or "" for s, e, cls, fid in docs if cls == "1"}
90
+ out = []
91
+ for doc in docs:
92
+ start, end, _, _ = doc
93
+ for i in range(start, end):
94
+ m = _GUID_RE.search(lines[i].encode("utf-8", "replace"))
95
+ if m is None or m.group(1).decode() not in guids:
96
+ continue
97
+ guid = m.group(1).decode()
98
+ field = yaml_path(lines, i, start)
99
+ if field.endswith("objectReference"): # a prefab override names its property on the line above
100
+ prop = _value_of(lines, max(start, i - 2), i, "propertyPath")
101
+ field = prop or field
102
+ out.append([guid, _object_of(ws, lines, doc, go_names), field])
103
+ return out
104
+
105
+
106
+ def graph_refs(ws) -> Dict[str, List[AssetRef]]:
107
+ """``graph guid -> references`` from every ``.asset`` / ``.prefab`` / ``.unity`` (computed once per run)."""
108
+ cached = getattr(ws, "_asset_refs", None)
109
+ if cached is not None:
110
+ return cached
111
+ paths = {ws.project.path_to_guid(s.path): s.path for s in ws.slots if not s.bound}
112
+ paths.pop(None, None)
113
+ guids = set(paths)
114
+ digest = hashlib.sha1(",".join(sorted(guids)).encode()).hexdigest()[:12]
115
+ cache = Cache(ws.project.root, "graph-refs")
116
+ if cache.data.get(_CACHE_KEY) != digest:
117
+ cache.data = {_CACHE_KEY: digest}
118
+ cache.dirty = True
119
+ out: Dict[str, List[AssetRef]] = {}
120
+ seen = []
121
+ needles = [g.encode() for g in guids]
122
+ for f in ws.project.iter_files(CONTAINER_SUFFIXES):
123
+ rel = ws.project.rel(f)
124
+ seen.append(rel)
125
+ st = f.stat()
126
+ hits = cache.get(rel, st)
127
+ if hits is None:
128
+ data = f.read_bytes()
129
+ hits = []
130
+ if any(n in data for n in needles):
131
+ hits = _scan_file(ws, rel, data.decode("utf-8", "replace").replace("\r\n", "\n"), guids)
132
+ cache.put(rel, st, hits)
133
+ for guid, obj, field in hits:
134
+ if paths.get(guid) != rel: # a graph naming itself is not a use
135
+ out.setdefault(guid, []).append(AssetRef(rel, obj, field))
136
+ cache.prune(seen + [_CACHE_KEY])
137
+ cache.save()
138
+ ws._asset_refs = out
139
+ return out
140
+
141
+
142
+ def _used_in_graphs(ws, ref: AssetRef, guid: str) -> bool:
143
+ """An entry in another graph's object list counts only if that graph uses it for something other than a
144
+ subgraph (which `owners` shows as a parent): a stale entry is no use at all."""
145
+ from .model import NESTED_KEYS
146
+ from .typedwalk import object_ref_usage
147
+
148
+ for slot in ws.slots:
149
+ if slot.path != ref.path:
150
+ continue
151
+ g = ws.load(slot)
152
+ if any(
153
+ isinstance(n.get(k), dict) and (g.ref(n[k].get("_value")) or ObjectRef(0)).guid == guid
154
+ for n in g.nodes
155
+ for k in NESTED_KEYS
156
+ ):
157
+ continue # shown as "subgraph of"
158
+ usage = object_ref_usage(ws, g)
159
+ used = {h[k] for h, k in usage.slots} | usage.maybe
160
+ if usage.unsafe or any(r.guid == guid and i in used for i, r in enumerate(g.refs)):
161
+ return True
162
+ return False
163
+
164
+
165
+ def refs_to(ws, path: str) -> List[AssetRef]:
166
+ """References to the graph asset at ``path`` that `owners` shows nowhere else."""
167
+ from .owners import index
168
+
169
+ guid = ws.project.path_to_guid(path)
170
+ idx = index(ws)
171
+ runs_it = {o.path for o in idx.owners if o.graph is not None and o.graph.guid == guid}
172
+ runs_it |= {i.path for i in idx.instances if i.graph is not None and i.graph.guid == guid}
173
+ out = []
174
+ for ref in graph_refs(ws).get(guid, []):
175
+ field = ref.field.split(".")[-1]
176
+ if field == "_graph" and ref.path in runs_it:
177
+ continue
178
+ if field in GRAPH_REF_LISTS and not _used_in_graphs(ws, ref, guid):
179
+ continue
180
+ out.append(ref)
181
+ return out
@@ -15,6 +15,7 @@ from . import fsjson
15
15
  from .container import ObjectRef, UnityFile
16
16
  from .model import BT, DT, FSM, Graph, walk
17
17
  from .schema import generic_args, simple_name
18
+ from .typedwalk import is_write_field, node_ref_slots, prune_refs, variable_ref_slots
18
19
  from .views import render_graph
19
20
  from .yamlscalar import encode
20
21
 
@@ -24,21 +25,6 @@ DERIVED = {
24
25
  "fsm": None,
25
26
  "dt": {"actorParameters": [], "$type": DT + "+DerivedSerializationData"},
26
27
  }
27
- NESTED_KEYS = ("_subTree", "_nestedBT", "_nestedFSM", "_nestedDLG", "_subGraph")
28
- WRITE_FIELDS = {
29
- "saveAs",
30
- "returnValue",
31
- "current",
32
- "storeIndex",
33
- "boolVariable",
34
- "floatVariable",
35
- "intVariable",
36
- "saveIndexAs",
37
- "saveDistanceAs",
38
- "saveObjectAs",
39
- "savePositionAs",
40
- "saveTo",
41
- }
42
28
  META = (
43
29
  "fileFormatVersion: 2\nguid: {guid}\nNativeFormatImporter:\n externalObjects: {{}}\n"
44
30
  " mainObjectFileID: 11400000\n userData: \n assetBundleName: \n assetBundleVariant: \n"
@@ -205,7 +191,7 @@ def used_variables(g: Graph, nodes: List[dict]) -> Dict[str, str]:
205
191
  continue
206
192
  key = path[-1] if path else ""
207
193
  owner_type = _owner_type(n, path)
208
- write = key in WRITE_FIELDS or (key == "valueA" and simple_name(owner_type).startswith("Set"))
194
+ write = is_write_field(key, owner_type)
209
195
  if isinstance(key, int) and len(path) >= 2 and path[-2] == "_variablesMap":
210
196
  write = bool(d.get("_canRead"))
211
197
  mode = out.get(vid, "")
@@ -223,54 +209,18 @@ def _owner_type(node: dict, path: Tuple) -> str:
223
209
 
224
210
 
225
211
  def object_ref_slots(ws, g: Graph, node: dict) -> List[Tuple[Any, Any]]:
226
- """``(container, key)`` of every int in ``node`` that indexes the graph's ``_objectReferences``:
227
- fields of a UnityEngine.Object type (also inside BBParameters, lists, nested classes and tasks),
228
- found by the field types in the sources."""
229
- from .render import _bb_inner, _elem_type
212
+ """``(container, key)`` of every int in ``node`` that indexes the graph's ``_objectReferences``."""
213
+ return node_ref_slots(ws, node)
230
214
 
231
- schema, r = ws.schema, ws.renderer(g)
232
- out: Dict[Tuple[int, Any], Tuple[Any, Any]] = {}
233
215
 
234
- def is_object(t: Optional[str], ctx) -> bool:
235
- return bool(t) and "<" not in t and not t.endswith("[]") and r._is_unity_object(t, ctx)
236
-
237
- def visit(v: Any, t: Optional[str], ctx, holder: Any, key: Any) -> None:
238
- if isinstance(v, bool) or v is None:
239
- return
240
- if isinstance(v, int):
241
- if is_object(t, ctx):
242
- out[(id(holder), key)] = (holder, key)
243
- return
244
- if isinstance(v, list):
245
- et = _elem_type(t)
246
- for i, x in enumerate(v):
247
- visit(x, et, ctx, v, i)
248
- return
249
- if not isinstance(v, dict):
250
- return
251
- if "$content" in v:
252
- visit(v["$content"], v.get("$type") or t, ctx, v, "$content")
253
- return
254
- if "_value" in v and (_bb_inner(t) or "_type" in v or (t or "").startswith("BBObjectParameter")):
255
- if not (v.get("_name") or v.get("_targetVariableID")):
256
- inner = v.get("_type") or _bb_inner(t)
257
- visit(v["_value"], inner.split(",")[0] if inner else None, ctx, v, "_value")
258
- return
259
- dt = v.get("$type") or t
260
- decl = schema.find(dt) if dt else None
261
- if decl is None and dt:
262
- decl = schema.find_simple(simple_name(dt).split("<")[0], ctx)
263
- fields = {n: (f, c) for n, f, c in schema.ordered_fields(decl.full_name)} if decl is not None else {}
264
- for k, x in v.items():
265
- f = fields.get(k)
266
- visit(x, f[0].type if f else None, f[1] if f else ctx, v, k)
267
-
268
- visit(node, None, None, None, None)
269
- for k in NESTED_KEYS: # subgraph references, even when the node type is not in the sources
270
- v = node.get(k)
271
- if isinstance(v, dict) and isinstance(v.get("_value"), int):
272
- out[(id(v), "_value")] = (v, "_value")
273
- return list(out.values())
216
+ def copy_variable(ws, src: Graph, dst: Graph, var: dict) -> dict:
217
+ """A deep copy of a blackboard variable for ``dst``; an object value keeps pointing at the same object."""
218
+ nv = copy.deepcopy(var)
219
+ if src is not dst:
220
+ for holder, key in variable_ref_slots(ws, nv):
221
+ ref = src.ref(holder[key])
222
+ holder[key] = dst.ref_index(ref) if ref is not None and not ref.is_null else 0
223
+ return nv
274
224
 
275
225
 
276
226
  def clone(
@@ -363,6 +313,7 @@ def _finish(ws, a, created: List[Tuple[UnityFile, Graph, str]], changed: List[Gr
363
313
  print("dry run: nothing written")
364
314
  return 0
365
315
  for f, g, guid in created:
316
+ prune_refs(ws, g)
366
317
  f.store(g.loaded)
367
318
  f.abspath.parent.mkdir(parents=True, exist_ok=True)
368
319
  f.dirty = True
@@ -429,9 +380,10 @@ def cmd_extract(ws, a) -> int:
429
380
  var_map: Dict[str, dict] = {}
430
381
  for name, v in g.variables.items():
431
382
  if v.get("_id") in usage:
432
- nv = {k: copy.deepcopy(x) for k, x in v.items() if k not in ("_isPublic", "$type")}
383
+ nv = copy_variable(ws, g, sub, v)
384
+ nv.pop("_isPublic", None)
433
385
  nv["_isPublic"] = True
434
- nv["$type"] = v["$type"]
386
+ nv["$type"] = nv.pop("$type")
435
387
  sub.blackboard["_variables"][name] = nv
436
388
  var_map[v["_id"]] = nv
437
389
  copies, conns = clone(ws, g, sub, nodes, var_map)
@@ -502,7 +454,8 @@ def cmd_inline(ws, a) -> int:
502
454
  new_name = name
503
455
  while g.var_by_name(new_name) is not None:
504
456
  new_name = f"{new_name} ({sub.name})"
505
- nv = {k: copy.deepcopy(x) for k, x in v.items() if k not in ("_isPublic",)}
457
+ nv = copy_variable(ws, sub, g, v)
458
+ nv.pop("_isPublic", None)
506
459
  nv["_name"] = new_name
507
460
  nv["_id"] = v["_id"] if g.var_by_id(v["_id"]) is None else str(uuid.uuid4())
508
461
  g.blackboard["_variables"][new_name] = nv
@@ -580,7 +533,7 @@ def copy_subtree(
580
533
  if as_root and dst.nodes and dst.kind == "bt":
581
534
  raise AssetError(f"{dst.name} already has a root; give a parent node to attach the copy to")
582
535
  for v in missing:
583
- nv = copy.deepcopy(v)
536
+ nv = copy_variable(ws, src, dst, v)
584
537
  nv.pop("_isPublic", None)
585
538
  if public_vars(v["_name"]) if callable(public_vars) else public_vars:
586
539
  nv["_isPublic"] = True