nnnotes 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.
Files changed (79) hide show
  1. nnnotes/__init__.py +7 -0
  2. nnnotes/__main__.py +4 -0
  3. nnnotes/acb.py +190 -0
  4. nnnotes/addressables.py +413 -0
  5. nnnotes/adv.py +243 -0
  6. nnnotes/advcommand.py +29 -0
  7. nnnotes/advmedia.py +178 -0
  8. nnnotes/advscene.py +113 -0
  9. nnnotes/advui.py +1154 -0
  10. nnnotes/advvideo.py +684 -0
  11. nnnotes/atoms/__init__.py +151 -0
  12. nnnotes/atoms/astc.py +52 -0
  13. nnnotes/atoms/gltf.py +290 -0
  14. nnnotes/atoms/mesh.py +289 -0
  15. nnnotes/atoms/png.py +28 -0
  16. nnnotes/atoms/reader.py +269 -0
  17. nnnotes/atoms/sniff.py +198 -0
  18. nnnotes/atoms/sprite.py +236 -0
  19. nnnotes/atoms/texture.py +80 -0
  20. nnnotes/cache.py +289 -0
  21. nnnotes/catalog.py +373 -0
  22. nnnotes/catalogdb.py +457 -0
  23. nnnotes/census.py +276 -0
  24. nnnotes/cli.py +744 -0
  25. nnnotes/cli_assets.py +1539 -0
  26. nnnotes/config.py +238 -0
  27. nnnotes/contract.py +497 -0
  28. nnnotes/cri.py +371 -0
  29. nnnotes/crikey.py +71 -0
  30. nnnotes/crilips.py +265 -0
  31. nnnotes/cristages.py +558 -0
  32. nnnotes/deckdata.py +462 -0
  33. nnnotes/export.py +1462 -0
  34. nnnotes/gameapi.py +318 -0
  35. nnnotes/jsonio.py +98 -0
  36. nnnotes/languages.py +44 -0
  37. nnnotes/layout.py +596 -0
  38. nnnotes/link.py +418 -0
  39. nnnotes/live.py +115 -0
  40. nnnotes/live2d.py +235 -0
  41. nnnotes/liveaudio.py +331 -0
  42. nnnotes/livenotes.py +291 -0
  43. nnnotes/liveoptions.py +250 -0
  44. nnnotes/livescene.py +450 -0
  45. nnnotes/liveui.py +328 -0
  46. nnnotes/master.py +251 -0
  47. nnnotes/modelstages.py +424 -0
  48. nnnotes/motion.py +131 -0
  49. nnnotes/objexport.py +1564 -0
  50. nnnotes/orchestrate.py +630 -0
  51. nnnotes/plan.py +218 -0
  52. nnnotes/player.py +392 -0
  53. nnnotes/room.py +391 -0
  54. nnnotes/score.py +1139 -0
  55. nnnotes/shader.py +217 -0
  56. nnnotes/spot.py +201 -0
  57. nnnotes/stages.py +285 -0
  58. nnnotes/store.py +518 -0
  59. nnnotes/story.py +142 -0
  60. nnnotes/storyfonts.py +1258 -0
  61. nnnotes/storyhost.py +1190 -0
  62. nnnotes/storysite.py +662 -0
  63. nnnotes/storyvoices.py +102 -0
  64. nnnotes/textstyle.py +355 -0
  65. nnnotes/tmpfont.py +636 -0
  66. nnnotes/typetrees/6000.3.12f1.json +19 -0
  67. nnnotes/unity.py +368 -0
  68. nnnotes/viewrules.json +441 -0
  69. nnnotes/views.py +966 -0
  70. nnnotes/voices.py +1363 -0
  71. nnnotes/web.py +1624 -0
  72. nnnotes/webaudio.py +14 -0
  73. nnnotes/webmodel.py +397 -0
  74. nnnotes-0.1.0.dist-info/METADATA +200 -0
  75. nnnotes-0.1.0.dist-info/RECORD +79 -0
  76. nnnotes-0.1.0.dist-info/WHEEL +5 -0
  77. nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
  78. nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. nnnotes-0.1.0.dist-info/top_level.txt +1 -0
nnnotes/census.py ADDED
@@ -0,0 +1,276 @@
1
+ """The census of a bundle (stage unity.census, document nnnotes.census/1): what a bundle holds, read without
2
+ decoding any object.
3
+
4
+ files the serialized files, sorted by name, each with its Unity version, platform, whether it embeds
5
+ type trees, its externals ({file, path, guid, type}; `file` is the name an object id uses: the
6
+ last component of the path, e.g. CAB-... or "unity default resources") and its objects sorted by
7
+ path id: {pathId, classId, class, byteSize, typeHash, name, script}. `name` is the object's m_Name
8
+ when its type has one (read without the fields after it), `script` the {file, pathId} of a
9
+ MonoBehaviour's m_Script (null for a null reference), `bindingScripts` the {file, pathId} of the
10
+ MonoScripts an AnimationClip's bindings name (MonoBehaviour fields it animates; sorted, only when
11
+ there are any). Objects are grouped per file: the files of one bundle may use the same path ids
12
+ (a scene bundle's scene and its shared assets).
13
+ resources the other files of the bundle (.resS / .resource): {name, size}
14
+ assetBundles the AssetBundle objects: {file, pathId, name, assetBundleName, dependencies,
15
+ isStreamedSceneAssetBundle, sceneHashes [[scene, hash]], container [{path, file, pathId,
16
+ preloadIndex, preloadSize}] (stored order)}
17
+ scripts the MonoScript objects: {file, pathId, assembly, namespace, class, name}
18
+ classes objects per class; `objects` the total
19
+ errors [{object, message}] of the objects whose header could not be read
20
+
21
+ A census is a function of the bundle's bytes: it holds no file name, time or path.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ from . import contract
28
+ from .atoms import impl_id
29
+ from .contract import Cost, Input
30
+ from .stages import Output, Pending, Stage
31
+
32
+ _ADDRESS = re.compile(r"0x[0-9a-fA-F]{6,}")
33
+ HEAD_FIELDS = ("m_Name", "m_Script")
34
+ CLIP_FIELDS = ("m_Name", "m_ClipBindingConstant") # an AnimationClip is read up to its bindings
35
+
36
+
37
+ def _ext_file(path: str) -> str:
38
+ """The name an object id uses for an external file: the last component of its path."""
39
+ return path.replace("\\", "/").rsplit("/", 1)[-1]
40
+
41
+
42
+ def _message(e: BaseException) -> str:
43
+ return _ADDRESS.sub("0x?", f"{type(e).__name__}: {e}")[:300]
44
+
45
+
46
+ def _serialized(env) -> tuple[list, list]:
47
+ """(serialized files, [name, size] of the other files) of a loaded environment, walking nested bundles."""
48
+ files, resources = [], []
49
+
50
+ def walk(container):
51
+ for name, f in getattr(container, "files", {}).items():
52
+ if hasattr(f, "objects") and hasattr(f, "externals"):
53
+ files.append(f)
54
+ elif hasattr(f, "files"):
55
+ walk(f)
56
+ else:
57
+ size = getattr(f, "Length", None)
58
+ if size is None and hasattr(f, "bytes"):
59
+ size = len(f.bytes)
60
+ resources.append({"name": name, "size": size})
61
+
62
+ for f in getattr(env, "files", {}).values():
63
+ if hasattr(f, "objects") and hasattr(f, "externals"):
64
+ files.append(f)
65
+ else:
66
+ walk(f)
67
+ return sorted(files, key=lambda f: f.name), sorted(resources, key=lambda r: r["name"])
68
+
69
+
70
+ class _Refs:
71
+ """PPtr resolution within one serialized file."""
72
+
73
+ def __init__(self, sf):
74
+ self.name = sf.name
75
+ self.externals = [_ext_file(x.path) for x in sf.externals]
76
+
77
+ def file(self, file_id: int) -> str | None:
78
+ if file_id == 0:
79
+ return self.name
80
+ return self.externals[file_id - 1] if 0 < file_id <= len(self.externals) else None
81
+
82
+ def ref(self, p) -> dict | None:
83
+ """{file, pathId} of a PPtr read as a dict; None for a null reference. An unknown file index keeps it as
84
+ fileId."""
85
+ if not isinstance(p, dict) or not p.get("m_PathID"):
86
+ return None
87
+ fid = int(p.get("m_FileID", 0))
88
+ f = self.file(fid)
89
+ return {"file": f, "pathId": int(p["m_PathID"])} if f is not None else {"fileId": fid,
90
+ "pathId": int(p["m_PathID"])}
91
+
92
+
93
+ def _pairs(v) -> list:
94
+ """A typetree map as [(key, value)] (UnityPy gives lists of pairs)."""
95
+ out = []
96
+ for item in v or ():
97
+ try:
98
+ k, x = item
99
+ except (TypeError, ValueError):
100
+ continue
101
+ out.append((k, x))
102
+ return out
103
+
104
+
105
+ def _asset_bundle(d: dict, refs: _Refs, oid: tuple[str, int]) -> dict:
106
+ container = []
107
+ for path, info in _pairs(d.get("m_Container")):
108
+ info = info if isinstance(info, dict) else {}
109
+ target = refs.ref(info.get("asset")) or {}
110
+ container.append({"path": path, "file": target.get("file"), "pathId": target.get("pathId"),
111
+ "preloadIndex": info.get("preloadIndex"), "preloadSize": info.get("preloadSize")})
112
+ return {"file": oid[0], "pathId": oid[1], "name": d.get("m_Name"), "assetBundleName": d.get("m_AssetBundleName"),
113
+ "dependencies": list(d.get("m_Dependencies") or ()),
114
+ "isStreamedSceneAssetBundle": bool(d.get("m_IsStreamedSceneAssetBundle")),
115
+ "sceneHashes": [[k, v] for k, v in _pairs(d.get("m_SceneHashes"))], "container": container}
116
+
117
+
118
+ def _binding_scripts(d: dict, refs: "_Refs") -> list:
119
+ """The MonoScripts an AnimationClip's generic bindings name, {file, pathId} (or {fileId, pathId}), sorted."""
120
+ out = {}
121
+ for b in ((d.get("m_ClipBindingConstant") or {}).get("genericBindings") or ()):
122
+ r = refs.ref(b.get("script")) if isinstance(b, dict) else None
123
+ if r is not None:
124
+ out[(r.get("file") is None, r.get("file") or "", r.get("fileId", 0), r["pathId"])] = r
125
+ return [out[k] for k in sorted(out)]
126
+
127
+
128
+ def census_env(env) -> dict:
129
+ """The census document of a loaded UnityPy environment holding one bundle."""
130
+ from UnityPy.helpers.TypeTreeNode import TypeTreeNode
131
+ heads: dict[tuple, tuple] = {}
132
+
133
+ def head(node, fields=HEAD_FIELDS):
134
+ """The type tree cut after the last of its `fields` (None when it has none of them)."""
135
+ hit = heads.get((id(node), fields))
136
+ if hit is not None and hit[0] is node:
137
+ return hit[1]
138
+ names = [c.m_Name for c in node.m_Children]
139
+ last = max((names.index(n) for n in fields if n in names), default=None)
140
+ cut = None
141
+ if last is not None:
142
+ cut = TypeTreeNode(node.m_Level, node.m_Type, node.m_Name, node.m_ByteSize, node.m_Version,
143
+ m_MetaFlag=node.m_MetaFlag, m_Children=node.m_Children[:last + 1])
144
+ heads[(id(node), fields)] = (node, cut)
145
+ return cut
146
+
147
+ sfiles, resources = _serialized(env)
148
+ files, bundles, scripts, errors, classes = [], [], [], [], {}
149
+ for sf in sfiles:
150
+ refs = _Refs(sf)
151
+ objects = []
152
+ for pid in sorted(sf.objects):
153
+ o = sf.objects[pid]
154
+ try:
155
+ cls = o.type.name
156
+ except Exception:
157
+ cls = f"Class{o.class_id}"
158
+ st = getattr(o, "serialized_type", None)
159
+ th = getattr(st, "old_type_hash", None) if st is not None else None
160
+ rec = {"pathId": int(pid), "classId": int(o.class_id), "class": cls, "byteSize": int(o.byte_size),
161
+ "typeHash": th.hex() if th else None}
162
+ classes[cls] = classes.get(cls, 0) + 1
163
+ try:
164
+ node = o._get_typetree_node()
165
+ cut = head(node, CLIP_FIELDS if cls == "AnimationClip" else HEAD_FIELDS)
166
+ d = o.read_typetree(cut, check_read=False) if cut is not None else {}
167
+ if "m_Name" in d:
168
+ rec["name"] = d["m_Name"]
169
+ if "m_Script" in d:
170
+ rec["script"] = refs.ref(d["m_Script"])
171
+ if cls == "AnimationClip":
172
+ scripts_ = _binding_scripts(d, refs)
173
+ if scripts_:
174
+ rec["bindingScripts"] = scripts_
175
+ if cls == "AssetBundle":
176
+ bundles.append(_asset_bundle(o.read_typetree(), refs, (sf.name, int(pid))))
177
+ elif cls == "MonoScript":
178
+ m = o.read_typetree()
179
+ scripts.append({"file": sf.name, "pathId": int(pid), "assembly": m.get("m_AssemblyName"),
180
+ "namespace": m.get("m_Namespace"), "class": m.get("m_ClassName"),
181
+ "name": m.get("m_Name")})
182
+ except Exception as e:
183
+ errors.append({"object": contract.object_id(sf.name, pid), "message": _message(e)})
184
+ objects.append(rec)
185
+ files.append({"name": sf.name, "unityVersion": getattr(sf, "unity_version", None),
186
+ "platform": int(getattr(sf, "_m_target_platform", getattr(sf, "target_platform", 0)) or 0),
187
+ "typetree": bool(getattr(sf, "_enable_type_tree", True)),
188
+ "externals": [{"file": _ext_file(x.path), "path": x.path,
189
+ "guid": x.guid.hex() if getattr(x, "guid", None) else None,
190
+ "type": getattr(x, "type", None)} for x in sf.externals],
191
+ "objects": objects})
192
+ return {"schema": contract.CENSUS, "files": files, "resources": resources,
193
+ "assetBundles": sorted(bundles, key=lambda b: (b["file"], b["pathId"])),
194
+ "scripts": sorted(scripts, key=lambda s: (s["file"], s["pathId"])),
195
+ "classes": dict(sorted(classes.items())), "objects": sum(classes.values()), "errors": errors}
196
+
197
+
198
+ def census_file(path) -> dict:
199
+ """The census of a bundle file (decrypted UnityFS)."""
200
+ from . import unity
201
+ env = unity.load(path)
202
+ try:
203
+ return census_env(env)
204
+ finally:
205
+ del env
206
+
207
+
208
+ # ---------------------------------------------------------------- reading a census
209
+ def objects(doc: dict):
210
+ """(file, object record) of every object of a census, by file and path id."""
211
+ for f in doc["files"]:
212
+ for o in f["objects"]:
213
+ yield f["name"], o
214
+
215
+
216
+ def container(doc: dict) -> list[dict]:
217
+ """The container entries of every AssetBundle object of a census, in stored order."""
218
+ return [e for b in doc["assetBundles"] for e in b["container"]]
219
+
220
+
221
+ def script_refs(doc: dict) -> list[str]:
222
+ """The object ids of the MonoScripts a census's objects reference (a MonoBehaviour's m_Script, the scripts an
223
+ AnimationClip's bindings name), sorted."""
224
+ out = set()
225
+ for _, o in objects(doc):
226
+ for s in (o.get("script"), *o.get("bindingScripts", ())):
227
+ if s and s.get("file") is not None:
228
+ out.add(contract.object_id(s["file"], s["pathId"]))
229
+ return sorted(out)
230
+
231
+
232
+ def facts(doc: dict) -> dict:
233
+ """The facts a census artifact record carries (what planning reads without opening the document)."""
234
+ return {"files": len(doc["files"]), "objects": doc["objects"], "classes": doc["classes"],
235
+ "scripts": len(doc["scripts"]), "scriptRefs": len(script_refs(doc)), "errors": len(doc["errors"])}
236
+
237
+
238
+ def bundle_subjects(index: dict) -> dict[str, dict]:
239
+ """{census subject: bundle entry} of a catalog index: the subject of a bundle is its stable name (the full file
240
+ name for names whose stable names collide, as the index records them)."""
241
+ return {b["stable"]: b for b in index["bundles"]}
242
+
243
+
244
+ # ---------------------------------------------------------------- stage
245
+ class CensusStage(Stage):
246
+ """unity.census: one bundle -> its census. Subjects and inputs come from the fact "bundles": {subject: Input
247
+ (role "bundle": the decrypted bundle's sha256, size, name, locators)}; a bundle not fetched yet is Pending
248
+ (raised by the fact, or given as a Pending / None). Artifact "<task id>#census". The census
249
+ reads the bundle with UnityPy: its atom is the reader's ("reader", the UnityPy version)."""
250
+ name = "unity.census"
251
+ version = 2
252
+ ATOMS = {"reader": impl_id(("UnityPy",), 1)}
253
+
254
+ def subjects(self, env) -> list[str]:
255
+ return sorted(env.fact("bundles"))
256
+
257
+ def inputs(self, subject: str, env) -> list[Input]:
258
+ inp = env.fact("bundles")[subject]
259
+ if inp is None: # listed but not fetched yet
260
+ raise Pending(f"fetch:{subject}")
261
+ if isinstance(inp, Exception): # a bundle the orchestrator cannot give yet (Pending "fetch:...")
262
+ raise inp
263
+ if inp.role != "bundle":
264
+ raise ValueError(f"unity.census {subject}: input role {inp.role!r}, expected 'bundle'")
265
+ return [inp]
266
+
267
+ def estimate(self, subject: str, env, inputs: list[Input]) -> Cost:
268
+ size = sum(i.size for i in inputs)
269
+ return Cost(0.002 + size / 40e6, (32 << 20) + 12 * size)
270
+
271
+ def run(self, task, store) -> Output:
272
+ doc = census_file(store.open_input(task.input("bundle")))
273
+ content = store.add(contract.encode(doc), "json")
274
+ rec = contract.artifact(contract.artifact_id(task.id, "census"), content, contract.provenance(task),
275
+ {"kind": "unity.census", "format": "json", "facts": facts(doc)})
276
+ return Output([rec], [])