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.
- nnnotes/__init__.py +7 -0
- nnnotes/__main__.py +4 -0
- nnnotes/acb.py +190 -0
- nnnotes/addressables.py +413 -0
- nnnotes/adv.py +243 -0
- nnnotes/advcommand.py +29 -0
- nnnotes/advmedia.py +178 -0
- nnnotes/advscene.py +113 -0
- nnnotes/advui.py +1154 -0
- nnnotes/advvideo.py +684 -0
- nnnotes/atoms/__init__.py +151 -0
- nnnotes/atoms/astc.py +52 -0
- nnnotes/atoms/gltf.py +290 -0
- nnnotes/atoms/mesh.py +289 -0
- nnnotes/atoms/png.py +28 -0
- nnnotes/atoms/reader.py +269 -0
- nnnotes/atoms/sniff.py +198 -0
- nnnotes/atoms/sprite.py +236 -0
- nnnotes/atoms/texture.py +80 -0
- nnnotes/cache.py +289 -0
- nnnotes/catalog.py +373 -0
- nnnotes/catalogdb.py +457 -0
- nnnotes/census.py +276 -0
- nnnotes/cli.py +744 -0
- nnnotes/cli_assets.py +1539 -0
- nnnotes/config.py +238 -0
- nnnotes/contract.py +497 -0
- nnnotes/cri.py +371 -0
- nnnotes/crikey.py +71 -0
- nnnotes/crilips.py +265 -0
- nnnotes/cristages.py +558 -0
- nnnotes/deckdata.py +462 -0
- nnnotes/export.py +1462 -0
- nnnotes/gameapi.py +318 -0
- nnnotes/jsonio.py +98 -0
- nnnotes/languages.py +44 -0
- nnnotes/layout.py +596 -0
- nnnotes/link.py +418 -0
- nnnotes/live.py +115 -0
- nnnotes/live2d.py +235 -0
- nnnotes/liveaudio.py +331 -0
- nnnotes/livenotes.py +291 -0
- nnnotes/liveoptions.py +250 -0
- nnnotes/livescene.py +450 -0
- nnnotes/liveui.py +328 -0
- nnnotes/master.py +251 -0
- nnnotes/modelstages.py +424 -0
- nnnotes/motion.py +131 -0
- nnnotes/objexport.py +1564 -0
- nnnotes/orchestrate.py +630 -0
- nnnotes/plan.py +218 -0
- nnnotes/player.py +392 -0
- nnnotes/room.py +391 -0
- nnnotes/score.py +1139 -0
- nnnotes/shader.py +217 -0
- nnnotes/spot.py +201 -0
- nnnotes/stages.py +285 -0
- nnnotes/store.py +518 -0
- nnnotes/story.py +142 -0
- nnnotes/storyfonts.py +1258 -0
- nnnotes/storyhost.py +1190 -0
- nnnotes/storysite.py +662 -0
- nnnotes/storyvoices.py +102 -0
- nnnotes/textstyle.py +355 -0
- nnnotes/tmpfont.py +636 -0
- nnnotes/typetrees/6000.3.12f1.json +19 -0
- nnnotes/unity.py +368 -0
- nnnotes/viewrules.json +441 -0
- nnnotes/views.py +966 -0
- nnnotes/voices.py +1363 -0
- nnnotes/web.py +1624 -0
- nnnotes/webaudio.py +14 -0
- nnnotes/webmodel.py +397 -0
- nnnotes-0.1.0.dist-info/METADATA +200 -0
- nnnotes-0.1.0.dist-info/RECORD +79 -0
- nnnotes-0.1.0.dist-info/WHEEL +5 -0
- nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
- nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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], [])
|