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.
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.claude-plugin/plugin.json +1 -1
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/CHANGELOG.md +47 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/PKG-INFO +6 -4
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/README.md +5 -3
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/pyproject.toml +1 -1
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/SKILL.md +44 -10
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/references/ops.md +8 -3
- nodecanvas_cli-0.3.0/src/nodecanvas_cli/__init__.py +1 -0
- nodecanvas_cli-0.3.0/src/nodecanvas_cli/assetrefs.py +181 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/assets.py +19 -66
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/bindings.py +24 -60
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/build.py +38 -27
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/builtin.py +31 -5
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/cli.py +3 -3
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/csharp.py +19 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/discover.py +57 -27
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/lint.py +174 -46
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/mcp_server.py +21 -15
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/model.py +2 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/ops.py +124 -3
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/owners.py +26 -24
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/refactor.py +95 -17
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/refs.py +13 -2
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/render.py +2 -2
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/scaffold.py +6 -1
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/schema.py +168 -22
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/spec.py +10 -9
- nodecanvas_cli-0.3.0/src/nodecanvas_cli/typedwalk.py +337 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/typenames.py +82 -4
- nodecanvas_cli-0.3.0/src/nodecanvas_cli/typerel.py +281 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/verify.py +100 -20
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/views.py +12 -9
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/workspace.py +33 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs +2 -0
- nodecanvas_cli-0.3.0/tests/test_blackboard_ops.py +178 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_lint.py +76 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_mcp.py +4 -0
- nodecanvas_cli-0.3.0/tests/test_object_refs.py +177 -0
- nodecanvas_cli-0.3.0/tests/test_owners.py +252 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_refactor.py +58 -0
- nodecanvas_cli-0.3.0/tests/test_type_contracts.py +308 -0
- nodecanvas_cli-0.3.0/tests/test_verify.py +239 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/uv.lock +1 -1
- nodecanvas_cli-0.2.0/src/nodecanvas_cli/__init__.py +0 -1
- nodecanvas_cli-0.2.0/tests/test_owners.py +0 -116
- nodecanvas_cli-0.2.0/tests/test_verify.py +0 -131
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.claude-plugin/marketplace.json +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.gitattributes +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.github/workflows/release.yml +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/.gitignore +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/LICENSE +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/bin/nodecanvas-cli +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/scripts/roundtrip_check.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/skills/nodecanvas-cli/scripts/nodecanvas-cli +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/__main__.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/config.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/container.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/dialogue.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/explain.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/fsjson.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/layout.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/notation.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/project.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/unity_verify.cs.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/src/nodecanvas_cli/yamlscalar.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/conftest.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/generate.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Blackboards/Shared.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Blackboards/Shared.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/Bot.cs +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/Bot.cs.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/GuardOwner.cs +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Game/GuardOwner.cs.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_BT.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_BT.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Bot_FSM.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Broken_BT.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Broken_BT.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Patrol_BT.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Sub_BT.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Sub_BT.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Talk_DT.asset +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Graphs/Talk_DT.asset.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Guard.prefab +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Guard.prefab.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Sentry.prefab +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Prefabs/Sentry.prefab.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Scenes/World.unity +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Scenes/World.unity.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/Assets/Stubs/NodeCanvasStubs.cs.meta +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/fixtures/project/ProjectSettings/ProjectVersion.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Bot_BT.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Bot_FSM.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Broken_BT.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Patrol_BT.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Sentry.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Sub_BT.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/snapshots/Talk_DT.txt +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_apply.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_assets.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_codec.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_csharp.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_dialogue.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_errors.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_explain.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_launcher.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_read.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_scaffold.py +0 -0
- {nodecanvas_cli-0.2.0 → nodecanvas_cli-0.3.0}/tests/test_spec.py +0 -0
- {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.
|
|
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.
|
|
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),
|
|
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),
|
|
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.
|
|
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)
|
|
93
|
-
|
|
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]`
|
|
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,
|
|
164
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 =
|
|
383
|
+
nv = copy_variable(ws, g, sub, v)
|
|
384
|
+
nv.pop("_isPublic", None)
|
|
433
385
|
nv["_isPublic"] = True
|
|
434
|
-
nv["$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 =
|
|
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 =
|
|
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
|