nodecanvas-cli 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- nodecanvas_cli/__main__.py +5 -0
- nodecanvas_cli/assets.py +627 -0
- nodecanvas_cli/bindings.py +332 -0
- nodecanvas_cli/build.py +696 -0
- nodecanvas_cli/builtin.py +425 -0
- nodecanvas_cli/cli.py +330 -0
- nodecanvas_cli/config.py +57 -0
- nodecanvas_cli/container.py +215 -0
- nodecanvas_cli/csharp.py +521 -0
- nodecanvas_cli/dialogue.py +422 -0
- nodecanvas_cli/discover.py +155 -0
- nodecanvas_cli/explain.py +540 -0
- nodecanvas_cli/fsjson.py +100 -0
- nodecanvas_cli/layout.py +259 -0
- nodecanvas_cli/lint.py +421 -0
- nodecanvas_cli/mcp_server.py +305 -0
- nodecanvas_cli/model.py +236 -0
- nodecanvas_cli/notation.txt +6 -0
- nodecanvas_cli/ops.py +789 -0
- nodecanvas_cli/owners.py +833 -0
- nodecanvas_cli/project.py +168 -0
- nodecanvas_cli/refactor.py +398 -0
- nodecanvas_cli/refs.py +108 -0
- nodecanvas_cli/render.py +612 -0
- nodecanvas_cli/scaffold.py +261 -0
- nodecanvas_cli/schema.py +451 -0
- nodecanvas_cli/spec.py +363 -0
- nodecanvas_cli/typenames.py +193 -0
- nodecanvas_cli/unity_verify.cs.txt +135 -0
- nodecanvas_cli/verify.py +372 -0
- nodecanvas_cli/views.py +230 -0
- nodecanvas_cli/workspace.py +165 -0
- nodecanvas_cli/yamlscalar.py +235 -0
- nodecanvas_cli-0.1.0.dist-info/METADATA +228 -0
- nodecanvas_cli-0.1.0.dist-info/RECORD +39 -0
- nodecanvas_cli-0.1.0.dist-info/WHEEL +4 -0
- nodecanvas_cli-0.1.0.dist-info/entry_points.txt +2 -0
- nodecanvas_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
nodecanvas_cli/assets.py
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Creating graph assets and moving subtrees between graphs (``new``, ``extract``, ``inline``, ``copy``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import difflib
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from . import fsjson
|
|
14
|
+
from .container import ObjectRef, UnityFile
|
|
15
|
+
from .model import BT, DT, FSM, Graph, walk
|
|
16
|
+
from .schema import generic_args, simple_name
|
|
17
|
+
from .views import render_graph
|
|
18
|
+
from .yamlscalar import encode
|
|
19
|
+
|
|
20
|
+
GRAPH_TYPES = {"bt": BT, "fsm": FSM, "dt": DT}
|
|
21
|
+
DERIVED = {
|
|
22
|
+
"bt": {"repeat": True, "$type": BT + "+DerivedSerializationData"},
|
|
23
|
+
"fsm": None,
|
|
24
|
+
"dt": {"actorParameters": [], "$type": DT + "+DerivedSerializationData"},
|
|
25
|
+
}
|
|
26
|
+
NESTED_KEYS = ("_subTree", "_nestedBT", "_nestedFSM", "_nestedDLG", "_subGraph")
|
|
27
|
+
WRITE_FIELDS = {
|
|
28
|
+
"saveAs",
|
|
29
|
+
"returnValue",
|
|
30
|
+
"current",
|
|
31
|
+
"storeIndex",
|
|
32
|
+
"boolVariable",
|
|
33
|
+
"floatVariable",
|
|
34
|
+
"intVariable",
|
|
35
|
+
"saveIndexAs",
|
|
36
|
+
"saveDistanceAs",
|
|
37
|
+
"saveObjectAs",
|
|
38
|
+
"savePositionAs",
|
|
39
|
+
"saveTo",
|
|
40
|
+
}
|
|
41
|
+
META = (
|
|
42
|
+
"fileFormatVersion: 2\nguid: {guid}\nNativeFormatImporter:\n externalObjects: {{}}\n"
|
|
43
|
+
" mainObjectFileID: 11400000\n userData: \n assetBundleName: \n assetBundleVariant: \n"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AssetError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------- new asset
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _template_lines(ws, kind: str) -> Tuple[List[str], str]:
|
|
55
|
+
"""Header/footer of an existing asset of the same kind (exact Unity layout), or a built-in one."""
|
|
56
|
+
for s in ws.slots:
|
|
57
|
+
if s.bound or not s.path.endswith(".asset"):
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
g = ws.load(s)
|
|
61
|
+
except Exception:
|
|
62
|
+
continue
|
|
63
|
+
if g.kind == kind:
|
|
64
|
+
return list(ws.file(s.path).lines), s.path
|
|
65
|
+
decl = ws.schema.find(GRAPH_TYPES[kind])
|
|
66
|
+
script_guid = ws.project.path_to_guid(decl.file) if decl is not None else None
|
|
67
|
+
if not script_guid:
|
|
68
|
+
raise AssetError(f"cannot find the {GRAPH_TYPES[kind]} script in the project to reference from a new asset")
|
|
69
|
+
lines = [
|
|
70
|
+
"%YAML 1.1",
|
|
71
|
+
"%TAG !u! tag:unity3d.com,2011:",
|
|
72
|
+
"--- !u!114 &11400000",
|
|
73
|
+
"MonoBehaviour:",
|
|
74
|
+
" m_ObjectHideFlags: 0",
|
|
75
|
+
" m_CorrespondingSourceObject: {fileID: 0}",
|
|
76
|
+
" m_PrefabInstance: {fileID: 0}",
|
|
77
|
+
" m_PrefabAsset: {fileID: 0}",
|
|
78
|
+
" m_GameObject: {fileID: 0}",
|
|
79
|
+
" m_Enabled: 1",
|
|
80
|
+
" m_EditorHideFlags: 0",
|
|
81
|
+
f" m_Script: {{fileID: 11500000, guid: {script_guid}, type: 3}}",
|
|
82
|
+
" m_Name: X",
|
|
83
|
+
f" m_EditorClassIdentifier: NodeCanvas::{GRAPH_TYPES[kind]}",
|
|
84
|
+
" _serializedGraph: '{}'",
|
|
85
|
+
" _objectReferences: []",
|
|
86
|
+
" _graphSource:",
|
|
87
|
+
" _version: 3.33",
|
|
88
|
+
" _category: ",
|
|
89
|
+
" _comments: ",
|
|
90
|
+
" _translation: {x: 0, y: 0}",
|
|
91
|
+
" _zoomFactor: 1",
|
|
92
|
+
" _haltSerialization: 0",
|
|
93
|
+
" _externalSerializationFile: {fileID: 0}",
|
|
94
|
+
"",
|
|
95
|
+
]
|
|
96
|
+
return lines, ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def make_asset(ws, rel: str, kind: str, data: dict, refs: List[ObjectRef]) -> UnityFile:
|
|
100
|
+
"""Build (in memory) a new graph asset file at project-relative ``rel``."""
|
|
101
|
+
lines, _ = _template_lines(ws, kind)
|
|
102
|
+
out = []
|
|
103
|
+
i = 0
|
|
104
|
+
name = Path(rel).stem
|
|
105
|
+
while i < len(lines):
|
|
106
|
+
line = lines[i]
|
|
107
|
+
st = line.lstrip(" ")
|
|
108
|
+
if st.startswith("m_Name:"):
|
|
109
|
+
out.append(" m_Name: " + name)
|
|
110
|
+
elif st.startswith("_serializedGraph:"):
|
|
111
|
+
out += encode("_serializedGraph", 2, fsjson.dumps(data))
|
|
112
|
+
i += 1
|
|
113
|
+
while i < len(lines) and not lines[i].lstrip(" ").startswith("_objectReferences:"):
|
|
114
|
+
i += 1
|
|
115
|
+
continue
|
|
116
|
+
elif st.startswith("_objectReferences:"):
|
|
117
|
+
out.append(" _objectReferences:" if refs else " _objectReferences: []")
|
|
118
|
+
out += [" - " + r.dump() for r in refs]
|
|
119
|
+
i += 1
|
|
120
|
+
while i < len(lines) and lines[i].startswith(" - "):
|
|
121
|
+
i += 1
|
|
122
|
+
continue
|
|
123
|
+
elif st.startswith("_translation:"):
|
|
124
|
+
out.append(" _translation: {x: 0, y: 0}")
|
|
125
|
+
elif st.startswith("_zoomFactor:"):
|
|
126
|
+
out.append(" _zoomFactor: 1")
|
|
127
|
+
elif st.startswith(("_category:", "_comments:")):
|
|
128
|
+
out.append(" " + st.split(":")[0] + ": ")
|
|
129
|
+
else:
|
|
130
|
+
out.append(line)
|
|
131
|
+
i += 1
|
|
132
|
+
data_bytes = "\n".join(out).encode("utf-8")
|
|
133
|
+
return UnityFile(ws.project.abs(rel), rel, data_bytes)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def empty_graph(kind: str) -> dict:
|
|
137
|
+
return {
|
|
138
|
+
"type": GRAPH_TYPES[kind],
|
|
139
|
+
"nodes": [],
|
|
140
|
+
"connections": [],
|
|
141
|
+
"canvasGroups": [],
|
|
142
|
+
"localBlackboard": {"_variables": {}},
|
|
143
|
+
"derivedData": copy.deepcopy(DERIVED[kind]),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _check_new_path(ws, rel: str, allow_outside: bool = False) -> None:
|
|
148
|
+
if not rel.endswith(".asset"):
|
|
149
|
+
raise AssetError("the new graph path must end with .asset")
|
|
150
|
+
if rel.startswith("/") or rel.startswith(".."):
|
|
151
|
+
raise AssetError("the new graph must be inside the Unity project")
|
|
152
|
+
if not (rel.startswith("Assets/") or rel.startswith("Packages/")):
|
|
153
|
+
if not allow_outside:
|
|
154
|
+
raise AssetError("the new graph must be inside Assets/ or Packages/ (or pass --allow-outside)")
|
|
155
|
+
print(f"note: {rel} is outside Assets/ and Packages/; Unity will not import it", file=sys.stderr)
|
|
156
|
+
if ws.project.abs(rel).exists():
|
|
157
|
+
raise AssetError(f"{rel} already exists")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _rel(ws, path: str) -> str:
|
|
161
|
+
p = Path(path)
|
|
162
|
+
return ws.project.rel(p if p.is_absolute() else Path.cwd() / p)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------- copying subtrees
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def subtree_nodes(g: Graph, root: dict) -> List[dict]:
|
|
169
|
+
order, seen = [], set()
|
|
170
|
+
|
|
171
|
+
def visit(n):
|
|
172
|
+
if id(n) in seen:
|
|
173
|
+
return
|
|
174
|
+
seen.add(id(n))
|
|
175
|
+
order.append(n)
|
|
176
|
+
for k in g.children(n):
|
|
177
|
+
visit(k)
|
|
178
|
+
|
|
179
|
+
visit(root)
|
|
180
|
+
inside = {id(n) for n in order}
|
|
181
|
+
for n in order[1:]:
|
|
182
|
+
outside = [p for p in g.parents(n) if id(p) not in inside]
|
|
183
|
+
if outside:
|
|
184
|
+
raise AssetError(
|
|
185
|
+
f"#{g.nid(n)} also has a parent outside the subtree (#{g.nid(outside[0])}); "
|
|
186
|
+
"move or remove that link first"
|
|
187
|
+
)
|
|
188
|
+
return order
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def used_variables(g: Graph, nodes: List[dict]) -> Dict[str, str]:
|
|
192
|
+
"""``variable id -> "r" | "w" | "rw"`` for variables referenced by ``nodes``."""
|
|
193
|
+
out: Dict[str, str] = {}
|
|
194
|
+
for n in nodes:
|
|
195
|
+
for path, d in walk(n):
|
|
196
|
+
vid = d.get("_targetVariableID")
|
|
197
|
+
if not vid or g.var_by_id(vid) is None:
|
|
198
|
+
continue
|
|
199
|
+
key = path[-1] if path else ""
|
|
200
|
+
owner_type = _owner_type(n, path)
|
|
201
|
+
write = key in WRITE_FIELDS or (key == "valueA" and simple_name(owner_type).startswith("Set"))
|
|
202
|
+
if isinstance(key, int) and len(path) >= 2 and path[-2] == "_variablesMap":
|
|
203
|
+
write = bool(d.get("_canRead"))
|
|
204
|
+
mode = out.get(vid, "")
|
|
205
|
+
out[vid] = "".join(sorted(set(mode + ("w" if write else "r"))))
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _owner_type(node: dict, path: Tuple) -> str:
|
|
210
|
+
cur, t = node, node.get("$type", "")
|
|
211
|
+
for p in path[:-1]:
|
|
212
|
+
cur = cur[p]
|
|
213
|
+
if isinstance(cur, dict) and "$type" in cur:
|
|
214
|
+
t = cur["$type"]
|
|
215
|
+
return t
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def clone(
|
|
219
|
+
src: Graph, dst: Graph, nodes: List[dict], var_map: Dict[str, dict], keep_uid: bool = True
|
|
220
|
+
) -> Tuple[List[dict], List[dict]]:
|
|
221
|
+
"""Deep-copy ``nodes`` (with their internal connections) for ``dst``: variable ids and object refs remapped.
|
|
222
|
+
|
|
223
|
+
Returned connections use indices into the returned node list.
|
|
224
|
+
"""
|
|
225
|
+
index = {id(n): i for i, n in enumerate(nodes)}
|
|
226
|
+
copies = [copy.deepcopy(n) for n in nodes]
|
|
227
|
+
for c in copies:
|
|
228
|
+
c.pop("$id", None)
|
|
229
|
+
if not keep_uid:
|
|
230
|
+
c.pop("_UID", None)
|
|
231
|
+
for _, d in walk(c):
|
|
232
|
+
vid = d.get("_targetVariableID")
|
|
233
|
+
if vid and vid in var_map:
|
|
234
|
+
d["_targetVariableID"] = var_map[vid]["_id"]
|
|
235
|
+
if "_name" in d:
|
|
236
|
+
d["_name"] = var_map[vid]["_name"]
|
|
237
|
+
for k in NESTED_KEYS:
|
|
238
|
+
v = c.get(k)
|
|
239
|
+
if isinstance(v, dict) and isinstance(v.get("_value"), int):
|
|
240
|
+
ref = src.ref(v["_value"])
|
|
241
|
+
v["_value"] = dst.ref_index(ref) if ref is not None and not ref.is_null else 0
|
|
242
|
+
conns = []
|
|
243
|
+
for n in nodes:
|
|
244
|
+
for c in src.out.get(src.nid(n), []):
|
|
245
|
+
t = src.by_id[c["_targetNode"]["$ref"]]
|
|
246
|
+
if id(t) in index:
|
|
247
|
+
cc = copy.deepcopy(c)
|
|
248
|
+
cc.pop("$id", None)
|
|
249
|
+
cc["_sourceNode"] = {"$ref": str(index[id(n)])}
|
|
250
|
+
cc["_targetNode"] = {"$ref": str(index[id(t)])}
|
|
251
|
+
conns.append(cc)
|
|
252
|
+
return copies, conns
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def graft(ed, copies: List[dict], conns: List[dict], parent: Optional[dict], index: Optional[int]) -> dict:
|
|
256
|
+
"""Append cloned nodes to the editor's graph and hang their root under ``parent``."""
|
|
257
|
+
g = ed.g
|
|
258
|
+
base = len(g.nodes)
|
|
259
|
+
g.nodes.extend(copies)
|
|
260
|
+
for c in conns:
|
|
261
|
+
c["_sourceNode"]["$ref"] = str(base + int(c["_sourceNode"]["$ref"]))
|
|
262
|
+
c["_targetNode"]["$ref"] = str(base + int(c["_targetNode"]["$ref"]))
|
|
263
|
+
g.connections.append(c)
|
|
264
|
+
g.reindex()
|
|
265
|
+
root = copies[0]
|
|
266
|
+
if parent is not None:
|
|
267
|
+
ed._link(parent, root, index)
|
|
268
|
+
return root
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------- commands
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _print_views(ws, before: Dict[str, List[str]], graphs: List[Graph]) -> None:
|
|
275
|
+
for g in graphs:
|
|
276
|
+
addr = g.loaded.slot.address
|
|
277
|
+
after = [re.sub(r"#\d+", "#", line) for line in render_graph(g, ws.renderer(g), with_header=True)[1:]]
|
|
278
|
+
print(f"== {addr}")
|
|
279
|
+
if addr in before:
|
|
280
|
+
diff = list(difflib.unified_diff(before[addr], after, "before", "after", n=1, lineterm=""))
|
|
281
|
+
print("\n".join(diff[2:]) if diff else "(no visible change)")
|
|
282
|
+
else:
|
|
283
|
+
print("\n".join(after))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _view(ws, g: Graph) -> List[str]:
|
|
287
|
+
return [re.sub(r"#\d+", "#", line) for line in render_graph(g, ws.renderer(g), with_header=True)[1:]]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _new_graph(ws, rel: str, kind: str) -> Tuple[UnityFile, Graph]:
|
|
291
|
+
f = make_asset(ws, rel, kind, empty_graph(kind), [])
|
|
292
|
+
slot = f.slots()[0]
|
|
293
|
+
return f, Graph(f.load(slot))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _finish(ws, a, created: List[Tuple[UnityFile, Graph, str]], changed: List[Graph]) -> int:
|
|
297
|
+
from .lint import ERROR, lint_graph
|
|
298
|
+
|
|
299
|
+
errors = [i for g in [c[1] for c in created] + changed for i in lint_graph(ws, g) if i.severity == ERROR]
|
|
300
|
+
for i in errors:
|
|
301
|
+
print("lint: " + i.line())
|
|
302
|
+
if errors and not getattr(a, "force", False):
|
|
303
|
+
print("not saved: new lint errors (use --force)", file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
if a.dry_run:
|
|
306
|
+
print("dry run: nothing written")
|
|
307
|
+
return 0
|
|
308
|
+
for f, g, guid in created:
|
|
309
|
+
f.store(g.loaded)
|
|
310
|
+
f.abspath.parent.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
f.dirty = True
|
|
312
|
+
f.save()
|
|
313
|
+
Path(str(f.abspath) + ".meta").write_text(META.format(guid=guid), encoding="utf-8")
|
|
314
|
+
where = f.relpath if f.relpath.startswith(("Assets/", "Packages/")) else str(f.abspath)
|
|
315
|
+
print(f"created {where} (guid {guid})")
|
|
316
|
+
for g in changed:
|
|
317
|
+
ws.save(g)
|
|
318
|
+
print(f"saved {g.loaded.slot.address}")
|
|
319
|
+
ws.project._containers = None
|
|
320
|
+
ws._slots = None
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def cmd_new(ws, a) -> int:
|
|
325
|
+
from .build import Builder
|
|
326
|
+
|
|
327
|
+
rel = _rel(ws, a.path)
|
|
328
|
+
_check_new_path(ws, rel, a.allow_outside)
|
|
329
|
+
f, g = _new_graph(ws, rel, a.kind)
|
|
330
|
+
b = Builder(ws, g)
|
|
331
|
+
for spec in a.var or []:
|
|
332
|
+
m = re.match(r"^\s*([^:=]+?)\s*:\s*([^=:]+?)\s*(?:=\s*([^:]*?))?\s*(:\s*pub(?:lic)?)?\s*$", spec)
|
|
333
|
+
if not m:
|
|
334
|
+
raise AssetError(f"--var {spec!r}: use Name:type[=value][:pub]")
|
|
335
|
+
value = None
|
|
336
|
+
if m.group(3):
|
|
337
|
+
import json as _json
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
value = _json.loads(m.group(3))
|
|
341
|
+
except ValueError:
|
|
342
|
+
value = m.group(3)
|
|
343
|
+
g.blackboard["_variables"][m.group(1)] = b.new_variable(m.group(1), m.group(2), value, bool(m.group(4)))
|
|
344
|
+
if a.source:
|
|
345
|
+
if ":" not in a.source:
|
|
346
|
+
raise AssetError("--from takes <graph>:<node>")
|
|
347
|
+
src_name, node_ref = a.source.rsplit(":", 1)
|
|
348
|
+
src = ws.graph(src_name)
|
|
349
|
+
_, created = copy_subtree(ws, src, node_ref, g, "root", None, True, public_vars=not a.private_vars)
|
|
350
|
+
if created:
|
|
351
|
+
print("created variables: " + ", ".join("$" + n for n in created))
|
|
352
|
+
_print_views(ws, {}, [g])
|
|
353
|
+
return _finish(ws, a, [(f, g, uuid.uuid4().hex)], [])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def cmd_extract(ws, a) -> int:
|
|
357
|
+
from .layout import Layouter
|
|
358
|
+
from .ops import apply_ops, set_position
|
|
359
|
+
|
|
360
|
+
g = ws.graph(a.graph, "bt")
|
|
361
|
+
root = g.node(a.node)
|
|
362
|
+
rel = _rel(ws, a.path)
|
|
363
|
+
_check_new_path(ws, rel, a.allow_outside)
|
|
364
|
+
nodes = subtree_nodes(g, root)
|
|
365
|
+
usage = used_variables(g, nodes)
|
|
366
|
+
before = {g.loaded.slot.address: _view(ws, g)}
|
|
367
|
+
|
|
368
|
+
f, sub = _new_graph(ws, rel, "bt")
|
|
369
|
+
sub.data["derivedData"] = copy.deepcopy(g.data.get("derivedData")) or sub.data["derivedData"]
|
|
370
|
+
var_map: Dict[str, dict] = {}
|
|
371
|
+
for name, v in g.variables.items():
|
|
372
|
+
if v.get("_id") in usage:
|
|
373
|
+
nv = {k: copy.deepcopy(x) for k, x in v.items() if k not in ("_isPublic", "$type")}
|
|
374
|
+
nv["_isPublic"] = True
|
|
375
|
+
nv["$type"] = v["$type"]
|
|
376
|
+
sub.blackboard["_variables"][name] = nv
|
|
377
|
+
var_map[v["_id"]] = nv
|
|
378
|
+
copies, conns = clone(g, sub, nodes, var_map)
|
|
379
|
+
sub.data["nodes"] = copies
|
|
380
|
+
sub.data["connections"] = conns
|
|
381
|
+
sub.reindex()
|
|
382
|
+
rx, ry = Layouter.pos(root) or (0.0, 0.0)
|
|
383
|
+
for c in copies:
|
|
384
|
+
p = Layouter.pos(c) or (0.0, 0.0)
|
|
385
|
+
set_position(c, p[0] - rx, p[1] - ry)
|
|
386
|
+
sub.renumber()
|
|
387
|
+
|
|
388
|
+
guid = uuid.uuid4().hex
|
|
389
|
+
ws.register_new(f, sub, guid)
|
|
390
|
+
ref_index = g.ref_index(ObjectRef(11400000, guid, 2))
|
|
391
|
+
mapping = []
|
|
392
|
+
order = [v.get("_id") for v in g.variables.values()]
|
|
393
|
+
for vid, mode in sorted(usage.items(), key=lambda kv: order.index(kv[0]) if kv[0] in order else len(order)):
|
|
394
|
+
v = g.var_by_id(vid)
|
|
395
|
+
vt = generic_args(v.get("$type", "")) or [None]
|
|
396
|
+
entry = {"_targetSubGraphVariableID": vid}
|
|
397
|
+
if "w" in mode:
|
|
398
|
+
entry["_canRead"] = True
|
|
399
|
+
if "r" in mode or "w" not in mode:
|
|
400
|
+
entry["_canWrite"] = True
|
|
401
|
+
entry["_type"] = vt[0].split(",")[0].strip() if vt[0] and "[[" not in vt[0] else vt[0]
|
|
402
|
+
entry["_name"] = v["_name"]
|
|
403
|
+
entry["_targetVariableID"] = vid
|
|
404
|
+
mapping.append(entry)
|
|
405
|
+
spec = {"$type": "SubTree", "_subTree": {"raw": {"_value": ref_index}}, "_variablesMap": {"raw": mapping}}
|
|
406
|
+
if a.name:
|
|
407
|
+
spec["_name"] = a.name
|
|
408
|
+
ops = [{"op": "remove", "node": g.nid(k), "recursive": True} for k in g.children(root)]
|
|
409
|
+
ops.append({"op": "replace", "node": g.nid(root), "with": spec})
|
|
410
|
+
apply_ops(ws, g, ops)
|
|
411
|
+
_print_views(ws, before, [sub, g])
|
|
412
|
+
return _finish(ws, a, [(f, sub, guid)], [g])
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def cmd_inline(ws, a) -> int:
|
|
416
|
+
from .ops import Editor, OpError
|
|
417
|
+
|
|
418
|
+
g = ws.graph(a.graph, "bt")
|
|
419
|
+
node = g.node(a.node)
|
|
420
|
+
if simple_name(node.get("$type", "")) != "SubTree":
|
|
421
|
+
raise AssetError(f"#{g.nid(node)} is not a SubTree")
|
|
422
|
+
ref = g.ref((node.get("_subTree") or {}).get("_value"))
|
|
423
|
+
sub = ws.ref_graph(ref) if ref is not None else None
|
|
424
|
+
if sub is None or not sub.nodes:
|
|
425
|
+
raise AssetError("the SubTree has no (or an empty) graph assigned")
|
|
426
|
+
before = {g.loaded.slot.address: _view(ws, g)}
|
|
427
|
+
var_map: Dict[str, dict] = {}
|
|
428
|
+
literal = []
|
|
429
|
+
for m in node.get("_variablesMap") or []:
|
|
430
|
+
sid = m.get("_targetSubGraphVariableID")
|
|
431
|
+
if m.get("_targetVariableID") and g.var_by_id(m["_targetVariableID"]) is not None:
|
|
432
|
+
var_map[sid] = g.var_by_id(m["_targetVariableID"])
|
|
433
|
+
elif "_value" in m:
|
|
434
|
+
literal.append(next((v.get("_name") for v in sub.variables.values() if v.get("_id") == sid), sid))
|
|
435
|
+
if literal:
|
|
436
|
+
raise AssetError(
|
|
437
|
+
f"mapped to constants: {', '.join(literal)}; give them parent variables first (map op), then inline"
|
|
438
|
+
)
|
|
439
|
+
created_vars = []
|
|
440
|
+
for name, v in sub.variables.items():
|
|
441
|
+
if v.get("_id") in var_map or "VariableSeperator" in v.get("$type", ""):
|
|
442
|
+
continue
|
|
443
|
+
new_name = name
|
|
444
|
+
while g.var_by_name(new_name) is not None:
|
|
445
|
+
new_name = f"{new_name} ({sub.name})"
|
|
446
|
+
nv = {k: copy.deepcopy(x) for k, x in v.items() if k not in ("_isPublic",)}
|
|
447
|
+
nv["_name"] = new_name
|
|
448
|
+
nv["_id"] = v["_id"] if g.var_by_id(v["_id"]) is None else str(uuid.uuid4())
|
|
449
|
+
g.blackboard["_variables"][new_name] = nv
|
|
450
|
+
var_map[v["_id"]] = nv
|
|
451
|
+
created_vars.append(new_name)
|
|
452
|
+
nodes = subtree_nodes(sub, sub.prime)
|
|
453
|
+
copies, conns = clone(sub, g, nodes, var_map)
|
|
454
|
+
ed = Editor(ws, g)
|
|
455
|
+
parent = (g.parents(node) or [None])[0]
|
|
456
|
+
old_pos = ed.lay.pos(node)
|
|
457
|
+
index = g.children(parent).index(node) if parent is not None else None
|
|
458
|
+
root = graft(ed, copies, conns, None, None)
|
|
459
|
+
sub_root_pos = ed.lay.pos(root) or (0.0, 0.0)
|
|
460
|
+
if old_pos is not None:
|
|
461
|
+
ed.lay.translate([root], old_pos[0] - sub_root_pos[0], old_pos[1] - sub_root_pos[1])
|
|
462
|
+
if parent is not None:
|
|
463
|
+
_, _, item = ed._unlink(parent, node)
|
|
464
|
+
ed._link(parent, root, index, item=item, has_item=item is not None)
|
|
465
|
+
ed._remove_node(node)
|
|
466
|
+
if parent is None:
|
|
467
|
+
ri = g.nodes.index(root)
|
|
468
|
+
g.nodes.insert(0, g.nodes.pop(ri))
|
|
469
|
+
ed._reorder_refs_after_move(ri)
|
|
470
|
+
ed.check_roots.append(root)
|
|
471
|
+
try:
|
|
472
|
+
ed.validate_structure()
|
|
473
|
+
except OpError as e:
|
|
474
|
+
raise AssetError(str(e)) from e
|
|
475
|
+
ed.layout()
|
|
476
|
+
ed.b.normalize(g.nodes)
|
|
477
|
+
g.renumber()
|
|
478
|
+
_print_views(ws, before, [g])
|
|
479
|
+
if created_vars:
|
|
480
|
+
print("created variables: " + ", ".join("$" + n for n in created_vars))
|
|
481
|
+
return _finish(ws, a, [], [g])
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def copy_subtree(
|
|
485
|
+
ws,
|
|
486
|
+
src: Graph,
|
|
487
|
+
node_ref: str,
|
|
488
|
+
dst: Graph,
|
|
489
|
+
parent_ref: str,
|
|
490
|
+
index: Optional[int],
|
|
491
|
+
create_vars: bool,
|
|
492
|
+
public_vars: bool = False,
|
|
493
|
+
):
|
|
494
|
+
"""Copy ``src``'s subtree at ``node_ref`` under ``parent_ref`` in ``dst`` (``root``: as the root of an
|
|
495
|
+
empty graph). Returns ``(editor, created variable names)``."""
|
|
496
|
+
from .ops import Editor, OpError
|
|
497
|
+
|
|
498
|
+
root = src.node(node_ref)
|
|
499
|
+
nodes = subtree_nodes(src, root)
|
|
500
|
+
usage = used_variables(src, nodes)
|
|
501
|
+
var_map: Dict[str, dict] = {}
|
|
502
|
+
missing = []
|
|
503
|
+
for vid in usage:
|
|
504
|
+
v = src.var_by_id(vid)
|
|
505
|
+
same = dst.var_by_id(vid) if dst is src else None
|
|
506
|
+
match = same or dst.var_by_name(v["_name"])
|
|
507
|
+
if match is not None and match.get("$type") == v.get("$type"):
|
|
508
|
+
var_map[vid] = match
|
|
509
|
+
elif match is not None:
|
|
510
|
+
raise AssetError(f"${v['_name']} exists in {dst.name} with another type")
|
|
511
|
+
else:
|
|
512
|
+
missing.append(v)
|
|
513
|
+
if missing and not create_vars:
|
|
514
|
+
raise AssetError(
|
|
515
|
+
"missing in the destination: "
|
|
516
|
+
+ ", ".join(f"${v['_name']}" for v in missing)
|
|
517
|
+
+ " (pass --create-vars to add them)"
|
|
518
|
+
)
|
|
519
|
+
as_root = str(parent_ref).lower() == "root"
|
|
520
|
+
if as_root and dst.nodes and dst.kind == "bt":
|
|
521
|
+
raise AssetError(f"{dst.name} already has a root; give a parent node to attach the copy to")
|
|
522
|
+
for v in missing:
|
|
523
|
+
nv = copy.deepcopy(v)
|
|
524
|
+
nv.pop("_isPublic", None)
|
|
525
|
+
if public_vars:
|
|
526
|
+
nv["_isPublic"] = True
|
|
527
|
+
nv["$type"] = nv.pop("$type")
|
|
528
|
+
if dst.var_by_id(nv["_id"]) is not None:
|
|
529
|
+
nv["_id"] = str(uuid.uuid4())
|
|
530
|
+
dst.blackboard["_variables"][nv["_name"]] = nv
|
|
531
|
+
var_map[v["_id"]] = nv
|
|
532
|
+
copies, conns = clone(src, dst, nodes, var_map, keep_uid=False)
|
|
533
|
+
ed = Editor(ws, dst)
|
|
534
|
+
was_empty = not dst.nodes
|
|
535
|
+
parent = None if as_root else (dst.node(parent_ref) if dst is not src else ed.node(parent_ref))
|
|
536
|
+
newroot = graft(ed, copies, conns, parent, index)
|
|
537
|
+
if parent is None:
|
|
538
|
+
if was_empty:
|
|
539
|
+
p = ed.lay.pos(newroot) or (0.0, 0.0)
|
|
540
|
+
ed.lay.translate([newroot], -p[0], -p[1])
|
|
541
|
+
ed.check_roots.append(newroot)
|
|
542
|
+
else:
|
|
543
|
+
ed.place_roots.append(newroot)
|
|
544
|
+
try:
|
|
545
|
+
ed.validate_structure()
|
|
546
|
+
except OpError as e:
|
|
547
|
+
raise AssetError(str(e)) from e
|
|
548
|
+
ed.layout()
|
|
549
|
+
ed.b.normalize(dst.nodes)
|
|
550
|
+
dst.renumber()
|
|
551
|
+
return ed, [v["_name"] for v in missing]
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def cmd_copy(ws, a) -> int:
|
|
555
|
+
src = ws.graph(a.src)
|
|
556
|
+
dst = src if a.dst in (a.src, ".") else ws.graph(a.dst)
|
|
557
|
+
before = {dst.loaded.slot.address: _view(ws, dst)}
|
|
558
|
+
ed, created = copy_subtree(ws, src, a.node, dst, a.parent, a.index, a.create_vars, a.public_vars)
|
|
559
|
+
_print_views(ws, before, [dst])
|
|
560
|
+
summary = ed.layout_summary()
|
|
561
|
+
if summary:
|
|
562
|
+
print(summary)
|
|
563
|
+
if created:
|
|
564
|
+
print("created variables: " + ", ".join("$" + n for n in created))
|
|
565
|
+
return _finish(ws, a, [], [dst])
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _guard(fn):
|
|
569
|
+
def run(ws, a):
|
|
570
|
+
try:
|
|
571
|
+
return fn(ws, a)
|
|
572
|
+
except AssetError as e:
|
|
573
|
+
print(f"error: {e}", file=sys.stderr)
|
|
574
|
+
return 1
|
|
575
|
+
|
|
576
|
+
return run
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def register(sub) -> None:
|
|
580
|
+
s = sub.add_parser("new", help="create a new graph asset (with .meta)")
|
|
581
|
+
s.add_argument("path", help="Assets/.../Name.asset")
|
|
582
|
+
s.add_argument("--kind", choices=["bt", "fsm", "dt"], required=True)
|
|
583
|
+
s.add_argument("--var", action="append", help='blackboard variable "Name:type[=value][:pub]", repeatable')
|
|
584
|
+
s.add_argument(
|
|
585
|
+
"--from",
|
|
586
|
+
dest="source",
|
|
587
|
+
help="start with a copy of <graph>:<node>; its variables are created public, "
|
|
588
|
+
"like extract does, so the graph is ready to be a SubTree",
|
|
589
|
+
)
|
|
590
|
+
s.add_argument("--private-vars", action="store_true", help="with --from: create the variables private")
|
|
591
|
+
s.add_argument("--dry-run", action="store_true")
|
|
592
|
+
s.add_argument("--force", action="store_true")
|
|
593
|
+
s.add_argument(
|
|
594
|
+
"--allow-outside", action="store_true", help="allow a path outside Assets/ and Packages/ (for experiments)"
|
|
595
|
+
)
|
|
596
|
+
s.set_defaults(func=_guard(cmd_new))
|
|
597
|
+
|
|
598
|
+
s = sub.add_parser("extract", help="move a BT subtree into a new SubTree asset and reference it in place")
|
|
599
|
+
s.add_argument("graph")
|
|
600
|
+
s.add_argument("node", help="root of the subtree")
|
|
601
|
+
s.add_argument("path", help="Assets/.../NewSubTree.asset")
|
|
602
|
+
s.add_argument("--name", help="node name for the SubTree")
|
|
603
|
+
s.add_argument("--dry-run", action="store_true")
|
|
604
|
+
s.add_argument("--force", action="store_true")
|
|
605
|
+
s.add_argument(
|
|
606
|
+
"--allow-outside", action="store_true", help="allow a path outside Assets/ and Packages/ (for experiments)"
|
|
607
|
+
)
|
|
608
|
+
s.set_defaults(func=_guard(cmd_extract))
|
|
609
|
+
|
|
610
|
+
s = sub.add_parser("inline", help="replace a SubTree node with a copy of its graph's nodes")
|
|
611
|
+
s.add_argument("graph")
|
|
612
|
+
s.add_argument("node", help="the SubTree node")
|
|
613
|
+
s.add_argument("--dry-run", action="store_true")
|
|
614
|
+
s.add_argument("--force", action="store_true")
|
|
615
|
+
s.set_defaults(func=_guard(cmd_inline))
|
|
616
|
+
|
|
617
|
+
s = sub.add_parser("copy", help="copy a subtree into another (or the same) graph")
|
|
618
|
+
s.add_argument("src")
|
|
619
|
+
s.add_argument("node")
|
|
620
|
+
s.add_argument("dst", help="destination graph (or . for the same graph)")
|
|
621
|
+
s.add_argument("parent", help="node in the destination to attach the copy to, or 'root' for an empty graph")
|
|
622
|
+
s.add_argument("--index", type=int)
|
|
623
|
+
s.add_argument("--create-vars", action="store_true", help="create missing variables in the destination")
|
|
624
|
+
s.add_argument("--public-vars", action="store_true", help="make variables created by --create-vars public")
|
|
625
|
+
s.add_argument("--dry-run", action="store_true")
|
|
626
|
+
s.add_argument("--force", action="store_true")
|
|
627
|
+
s.set_defaults(func=_guard(cmd_copy))
|