diff --git a/docs/assets.md b/docs/assets.md index 1eeb842..7060085 100644 --- a/docs/assets.md +++ b/docs/assets.md @@ -147,7 +147,8 @@ no longer wanted, never touches other files, and continues an interrupted write - other objects under the same container path: `stem[].`, the Addressables sub-object form (`…/member_full[square].png`); - objects without a container path: `_bundles///~.`; -- secondary artifacts beside the primary one: `..` (`….meta.json`, `….blob.m_Data.moc3`); +- secondary artifacts beside the primary one: `..` (`….blob.m_Data.moc3`), `.` for a + role that is its extension (`….png.json`, the `json` of a texture or sprite); - a container path present in two bundles: `stem@.ext` for each; - names Windows refuses, reserved device names and trailing dots are escaped as `%XX`; paths that coincide ignoring case get `~<8 hex>` and are listed in the layout report; diff --git a/docs/contracts.md b/docs/contracts.md index affcad8..389a28b 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -109,7 +109,8 @@ One output file ([artifact.schema.json](https://github.com/MetaSekaiLab/nnnotes/ `ext` is lower-case letters and digits. The bytes are stored once, by content id, whatever their names. Roles: an object's primary artifact is the one whose role comes first in `image`, `mesh`, `audio`, `video`, `font`, -`data`, `prefab`, `json` (then by role name); its other artifacts (`meta`, `blob:`, …) are secondary. +`data`, `json` (then by role name); its other artifacts (`json` beside an image, `blob:`, …) are +secondary. ## Result: nnnotes.result/1 @@ -120,7 +121,7 @@ The result of a task, written after the bytes of all its artifacts are stored: i {"schema": "nnnotes.result/1", "key": "…", "keyParts": {…}, "status": "ok", "artifacts": [], "items": [{"object": "CAB-…:1", "class": "Texture2D", "status": "exported", "artifacts": ["CAB-…:1#image"]}, - {"object": "CAB-…:2", "class": "Transform", "status": "contained", "in": "CAB-…:9#prefab"}, + {"object": "CAB-…:2", "class": "Transform", "status": "contained", "in": "CAB-…:9#json"}, {"object": "CAB-…:3", "class": "Texture2D", "status": "unsupported", "reason": {"code": "empty.texture", "message": "…"}}]} ``` diff --git a/docs/stages.md b/docs/stages.md index bcd1f01..f0473c6 100644 --- a/docs/stages.md +++ b/docs/stages.md @@ -48,21 +48,33 @@ with its reason code; any other error of a converter gives it with `generic.erro | Class | Converter | Artifacts | Atoms | Notes | |---|---|---|---|---| -| Texture2D | `tex.png/1` | `image`: PNG of the first mip level (HDR ASTC: `.astc`) | `texture.decode`, `png.encode`, `astc.container` | further mip levels as facts; a texture without pixels: `empty.texture` | -| Sprite | `sprite.png/1` | `image`: PNG on the sprite's rect (packing undone, tight mask applied), `meta`: JSON | `texture.decode`, `sprite.crop`, `png.encode`, `mesh.arrays` | the texture is decoded once per task; a sprite whose SpriteAtlas is in another bundle gets its `meta` here (with its mesh and object facts) and its `image` from `sprite.crop` | -| Mesh | `mesh.glb/1` | `mesh`: binary glTF in the mesh's local space | `mesh.arrays`, `gltf.write` | see `gltf.write` below; limits of the skin and blend shapes as issues of the artifact | -| GameObject (a root) | `prefab.json/1` | `prefab`: the hierarchy's nodes (path, path ids, transform) with their components' fields | | the objects below it are `contained`; a root of a scene's file names its scene | +| Texture2D | `tex.png/2` | `image`: PNG of the first mip level (HDR ASTC: `.astc`), `json`: the typetree without `image data` (settings, colour space, mip count, stream data) | `texture.decode`, `png.encode`, `astc.container` | further mip levels as facts; a texture without pixels: `empty.texture` | +| Sprite | `sprite.png/2` | `image`: PNG on the sprite's rect (packing undone, tight mask applied), `json`: the typetree, geometry, render data and image (see below) | `texture.decode`, `sprite.crop`, `png.encode`, `mesh.arrays` | the texture is decoded once per task; a sprite whose SpriteAtlas is in another bundle gets its `json` here (with its geometry and object facts) and its `image` from `sprite.crop` | +| Mesh | `mesh.glb/2` | `mesh`: binary glTF in the mesh's local space, `json`: the typetree without the vertex, index and compressed mesh data | `mesh.arrays`, `gltf.write` | see `gltf.write` below; limits of the skin and blend shapes as issues of the artifact | +| GameObject (a root) | `prefab.json/2` | `json`: the hierarchy's nodes (path, path ids, transform) with their components' fields | | the objects below it are `contained`; a root of a scene's file names its scene | | Material | `material.json/1` | `json`: shader, keywords, textures, floats, colours | | | | Shader | `shader.json/1` | `json`: the parsed form (the bytes of `nnnotes export shader`'s `.json`), `program:/sp

__`: each compiled sub-program as stored (`.glsl`, `.metal`, `.vkprog`), `index`: the variants (platform, subshader, pass, stage, GPU program type, keywords, program role), `typetree`: the typetree JSON | | a shader without a parsed form: its typetree with `generic.error` | | AnimationClip | `clip.json/2` | `json`: the Mecanim clip data and its bindings as stored (see below) | | a legacy or compressed clip: `generic.clip.legacy` | | AnimatorController | `controller.json/2` | `json`: name, clips (`$ref`), layers, state machines, parameters, default values | | an AnimatorOverrideController: `generic.controller.override` | | MonoBehaviour (not in a hierarchy) | `mono.json/2`, `cri.usm/1` | `json`: `$script` (assembly, namespace, class) and the typetree; a cue sheet asset also `acb`, a movie asset holding its USM also `usm` (see below) | `sniff` | the class from the task's script table | -| TextAsset | `data/1` | `data`: the stored bytes | `sniff` | extension from the container path, else from the content | -| Font | `font/1` | `font`: the font file | `sniff` | | +| TextAsset | `data/2` | `data`: the stored bytes, `json`: the typetree without `m_Script` | `sniff` | extension from the container path, else from the content | +| Font | `font/2` | `font`: the font file, `json`: the typetree without `m_FontData` | `sniff` | | | MonoScript | `scripts.json/1` | `#scripts.json`: every MonoScript of the bundle | | | | AssetBundle | `bundle.json/1` | `#bundle.json`: serialized files, externals, the AssetBundle objects | | | | any other class | `generic.json/1` | `json`: the typetree | `sniff` | | +Every object a converter takes has a `json` artifact or is contained in another object's. Each item lists in +`refs` the objects its documents reference (by object id, other bundles' objects included, the objects of its own +prefab left out): the objects reachable from an object follow from the items alone. + +A sprite's `json` holds `sprite` (its typetree), `mesh` (its geometry decoded: `positions` in the sprite's units, +`submeshes` of triangles, `uv0` when stored), `renderData` (`source`: `renderData` for the sprite's own render +data, `atlas` for its SpriteAtlas' entry; `atlas`; and that render data's `texture`, `alphaTexture`, +`secondaryTextures`, `textureRect`, `textureRectOffset`, `atlasRectOffset`, `uvTransform`, `downscaleMultiplier`, +`settingsRaw`) and `image` (`packed`, `packingMode`, `packingRotation`, `width`, `height`). The texture coordinate +of a position `p` is `((p.x * uvTransform.x + uvTransform.y) / width, (p.y * uvTransform.z + uvTransform.w) / +height)` of the render data's texture. + A task's key includes the atoms `reader` and `sniff`, the converter `generic.json`, and the converters (with their atoms) of the classes the bundle's census lists, so a converter's version bump runs again exactly the bundles holding its classes. `classes` (a list of class names) limits a task to the objects of those classes and the objects their @@ -110,7 +122,7 @@ the same way: its `data.data` bytes, whatever their size, are the artifact `:`). Its inputs are the atlas' JSON, the `meta` of each such sprite and the images of the +subject>:`). Its inputs are the atlas' JSON, the `json` of each such sprite and the images of the atlas textures they use; it writes `#image` with the same bytes `unity.export` writes for a sprite whose atlas is in its own bundle. diff --git a/src/nnnotes/pipeline/contract.py b/src/nnnotes/pipeline/contract.py index 69eb785..26fa343 100644 --- a/src/nnnotes/pipeline/contract.py +++ b/src/nnnotes/pipeline/contract.py @@ -63,7 +63,7 @@ # locator kinds of a task input: where the bytes can be read (never part of the key) LOCATOR_KINDS = ("depot", "download", "file", "catalog") # an object's primary artifact is the one whose role comes first here; its other artifacts are secondary -PRIMARY_ROLES = ("image", "mesh", "audio", "video", "font", "data", "prefab", "json") +PRIMARY_ROLES = ("image", "mesh", "audio", "video", "font", "data", "json") _EXT = re.compile(r"[0-9a-z]+") _BUNDLE_SUFFIX = re.compile(r"(_[0-9a-f]{32})?\.bundle$") @@ -324,11 +324,18 @@ def reason(code: str, message: str = "") -> dict: def item( - obj: str, status: str, *, artifacts=(), within: str | None = None, why: dict | None = None, cls: str | None = None + obj: str, + status: str, + *, + artifacts=(), + within: str | None = None, + why: dict | None = None, + cls: str | None = None, + refs=(), ) -> dict: """The outcome for one object of a task. exported: its artifacts; contained: `within` (the artifact it is part of); generic: its artifacts and why the specialized form was not made; unsupported: why (and artifacts, if - any); failed: why.""" + any); failed: why. `refs`: the ids of the other objects its documents reference.""" if status not in ITEM_STATUSES: raise ValueError(f"item {obj}: unknown status {status!r}") need_art = status in ("exported", "generic") @@ -348,6 +355,8 @@ def item( d["artifacts"] = sorted(artifacts) if within is not None: d["in"] = within + if refs: + d["refs"] = sorted(set(refs)) if why is not None: d["reason"] = why return d diff --git a/src/nnnotes/pipeline/layout.py b/src/nnnotes/pipeline/layout.py index cc9a3c2..3fbb13d 100644 --- a/src/nnnotes/pipeline/layout.py +++ b/src/nnnotes/pipeline/layout.py @@ -11,7 +11,8 @@ `~` (stem[name]~123.png) - an object's primary artifact (contract.PRIMARY_ROLES) is written at that path, its extension appended unless the path already ends with it (x.prefab -> x.prefab.json, y.png -> y.png); its other artifacts at - `..` (':' and '/' of the role as '.') + `..` (':' and '/' of the role as '.'), `.` for a role that is its extension + (y.png -> y.png.json) - objects without a container: `//~.`, root = the root of the object's serialized file when one is given (`object_roots`: the export command roots every object of a bundle at `_bundles/`, whichever task made the artifact), else the source's root; under a root whose @@ -301,7 +302,8 @@ def _object_files(base: list[str], records: list[dict], append_ext: bool) -> lis segs = base[:-1] + [f"{base[-1]}.{ext}"] if append_ext else _with_ext(base, ext) else: role = contract.parse_artifact_id(r["id"])[1] - segs = base[:-1] + [f"{base[-1]}.{_role_segment(role)}.{ext}"] + name = f"{base[-1]}.{ext}" if role == ext else f"{base[-1]}.{_role_segment(role)}.{ext}" + segs = base[:-1] + [name] out.append((segs, _entry("", r))) return out diff --git a/src/nnnotes/pipeline/objexport.py b/src/nnnotes/pipeline/objexport.py index f17904d..182e638 100644 --- a/src/nnnotes/pipeline/objexport.py +++ b/src/nnnotes/pipeline/objexport.py @@ -130,6 +130,7 @@ def _emit( def _begin(self, conv: str) -> None: self.staged, self.conv = [], conv + self.ctx.seen.clear() def _publish(self) -> list[str]: """Keep the staged artifacts; returns their ids.""" @@ -139,10 +140,15 @@ def _publish(self) -> list[str]: self.staged = [] return ids + def _refs(self, info) -> list[str]: + """The objects the documents of `info` made since the last _begin reference.""" + return sorted(self.ctx.seen - {info.id}) + def _commit(self, info, status: str, why: dict | None = None) -> None: - """The item of `info`: the artifacts its conversion staged.""" + """The item of `info`: the artifacts its conversion staged and the objects they reference.""" + refs = self._refs(info) ids = self._publish() - self.items[info.id] = contract.item(info.id, status, artifacts=ids, why=why, cls=info.class_name) + self.items[info.id] = contract.item(info.id, status, artifacts=ids, why=why, cls=info.class_name, refs=refs) def _failed(self, info, e: Exception, prefix: str = "") -> None: self._begin("generic.json") @@ -270,7 +276,7 @@ def _prefabs(self) -> None: self._fallback(info_of[k], "generic.error", f"hierarchy at transform {root.transform}: {why}") continue if self._convert(info, "prefab.json", prefab.prefab, h, root): - within = contract.artifact_id(info.id, "prefab") + within = contract.artifact_id(info.id, "json") for k in root.members: i = info_of[k] if i.id != info.id: @@ -293,9 +299,10 @@ def _loose(self, cls: str, conv: str, kind: str) -> None: for i in infos: mark = len(self.staged) key = (i.file, i.path_id) + self.ctx.seen.clear() try: entries[i.id] = self.ctx.tree(self.ctx.readers[key], self.ctx.tt(key)) - done.append(i) + done.append((i, self._refs(i))) except Exception as e: del self.staged[mark:] failed.append((i, e)) @@ -303,8 +310,8 @@ def _loose(self, cls: str, conv: str, kind: str) -> None: doc = encode_object(generic.loose_doc(self.ctx, conv, entries)) aid = self._emit(None, conv, doc, "json", kind, "json", {"objects": len(done)}, owner=self.task.id)[0] self._publish() - for i in done: - self.items[i.id] = contract.item(i.id, "contained", within=aid, cls=i.class_name) + for i, refs in done: + self.items[i.id] = contract.item(i.id, "contained", within=aid, cls=i.class_name, refs=refs) for i, e in failed: if isinstance(e, ReadError): self._failed(i, e) diff --git a/src/nnnotes/schemas/result.schema.json b/src/nnnotes/schemas/result.schema.json index 96557fa..541d034 100644 --- a/src/nnnotes/schemas/result.schema.json +++ b/src/nnnotes/schemas/result.schema.json @@ -68,6 +68,15 @@ "in": { "$ref": "urn:nnnotes:schema:common#/$defs/artifactId" }, + "refs": { + "description": "The ids of the other objects the object's documents reference, other bundles' objects included.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, "reason": { "$ref": "urn:nnnotes:schema:common#/$defs/reason" } diff --git a/src/nnnotes/unity/convert/__init__.py b/src/nnnotes/unity/convert/__init__.py index 1020360..759d8ab 100644 --- a/src/nnnotes/unity/convert/__init__.py +++ b/src/nnnotes/unity/convert/__init__.py @@ -6,9 +6,9 @@ bundle holds, with the atoms they use (CONVERTER_ATOMS). Texture2D tex.png `image`: PNG of the first mip level (HDR ASTC: the .astc file) - Sprite sprite.png `image`: PNG on the sprite's rect; `meta`: JSON + Sprite sprite.png `image`: PNG on the sprite's rect; `json`: typetree, geometry, render data Mesh mesh.glb `mesh`: binary glTF in the mesh's local space - GameObject (a root) prefab.json `prefab`: the hierarchy below it with its components + GameObject (a root) prefab.json `json`: the hierarchy below it with its components Material material.json `json` Shader shader.json `json`: the parsed form as `nnnotes export shader` writes it; `program:/sp

__`: each compiled sub-program as @@ -25,6 +25,9 @@ AssetBundle bundle.json "#bundle.json": serialized files, externals, AssetBundle objects any other class generic.json `json`: the object's typetree +Every object a converter takes has a `json` artifact (or is contained in one): the converter's document, or the +typetree JSON (a Texture2D, Mesh, TextAsset or Font without the bytes its other artifact holds). + Object JSON (canon/1 in the object's own field order): - a reference is {"$ref": {"file", "pathId"}}, with "class" and "name" when the target is in the bundle; a null reference is null. Inside a prefab, a GameObject, Transform or component of the same prefab is named by its path: @@ -60,16 +63,16 @@ # converter -> version CONVERTERS = { - "tex.png": 1, - "sprite.png": 1, - "mesh.glb": 1, - "prefab.json": 1, + "tex.png": 2, + "sprite.png": 2, + "mesh.glb": 2, + "prefab.json": 2, "material.json": 1, "clip.json": 2, "controller.json": 2, "mono.json": 2, - "data": 1, - "font": 1, + "data": 2, + "font": 2, "scripts.json": 1, "bundle.json": 1, "shader.json": 1, diff --git a/src/nnnotes/unity/convert/files.py b/src/nnnotes/unity/convert/files.py index 3c0269e..fe1509b 100644 --- a/src/nnnotes/unity/convert/files.py +++ b/src/nnnotes/unity/convert/files.py @@ -1,17 +1,26 @@ -"""Mesh (mesh.glb), TextAsset (data) and Font (font).""" +"""Mesh (mesh.glb), TextAsset (data) and Font (font). Each also has `json`: its typetree JSON without the bytes its +other artifact holds.""" from __future__ import annotations from ..atoms import Unsupported -from .objects import BundleObjects, container_ext +from .objects import BundleObjects, container_ext, encode_object, omit -__all__ = ["FONT_FORMATS", "font", "mesh", "text"] +__all__ = ["FONT_FORMATS", "MESH_DATA", "font", "mesh", "text"] FONT_FORMATS = ("ttf", "otf", "ttc", "woff", "woff2") +# the fields of a Mesh whose data its glTF holds decoded: vertices, indices, the compressed form +MESH_DATA = ("m_VertexData/m_DataSize", "m_IndexBuffer", "m_CompressedMesh") + + +def _json(ctx: BundleObjects, key: tuple, tt: dict, *data: str) -> None: + doc = ctx.tree(ctx.readers[key], omit(tt, *data)) + ctx.emit(key, "json", encode_object(doc), "json", "unity.object", "json", {}) def mesh(ctx: BundleObjects, info) -> None: - """`mesh`: binary glTF in the mesh's local space; the limits of its conversion as issues.""" + """`mesh`: binary glTF in the mesh's local space, the limits of its conversion as issues; `json` without the + vertex, index and compressed mesh data (MESH_DATA).""" key = (info.file, info.path_id) m = ctx.fns["mesh.arrays"](ctx.view.mesh_input(info)) glb = ctx.fns["gltf.write"]([m]) @@ -22,25 +31,31 @@ def mesh(ctx: BundleObjects, info) -> None: "skinned": m.skin is not None, } ctx.emit(key, "mesh", glb, "glb", "mesh.gltf", "glb", facts, issues=m.issues) + _json(ctx, key, ctx.tt(key), *MESH_DATA) def text(ctx: BundleObjects, info) -> None: - """`data`: the stored bytes, the extension from the container path, else from the content.""" + """`data`: the stored bytes, the extension from the container path, else from the content; `json` without + them (`m_Script`).""" key = (info.file, info.path_id) - s = ctx.tt(key).get("m_Script", "") + tt = ctx.tt(key) + s = tt.get("m_Script", "") data = s.encode("utf-8", "surrogateescape") if isinstance(s, str) else bytes(s or b"") container = ctx.containers.get(key) sn = ctx.fns["sniff"](data, container) ext = container_ext(container) or sn.ext ctx.emit(key, "data", data, ext, "text", sn.format, {"size": len(data)}) + _json(ctx, key, tt, "m_Script") def font(ctx: BundleObjects, info) -> None: - """`font`: the font file.""" + """`font`: the font file; `json` without it (`m_FontData`).""" key = (info.file, info.path_id) - data = bytes(ctx.tt(key).get("m_FontData") or b"") + tt = ctx.tt(key) + data = bytes(tt.get("m_FontData") or b"") if not data: raise Unsupported("no_data.font", "the font has no font data") sn = ctx.fns["sniff"](data) ext = sn.ext if sn.format in FONT_FORMATS else (container_ext(ctx.containers.get(key)) or sn.ext) ctx.emit(key, "font", data, ext, "font", sn.format, {"size": len(data)}) + _json(ctx, key, tt, "m_FontData") diff --git a/src/nnnotes/unity/convert/objects.py b/src/nnnotes/unity/convert/objects.py index e92c2ce..b75a0d1 100644 --- a/src/nnnotes/unity/convert/objects.py +++ b/src/nnnotes/unity/convert/objects.py @@ -4,6 +4,9 @@ Artifacts go out through `emit(key, role, data, ext, kind, format, facts, *, issues=(), refs=(), owner=None)`, which the caller gives: it stores the bytes and records the artifact "#" (owner: the object `key`, else the task-level name `owner`) and returns (artifact id, content record). + +Every object a document names by {"$ref"} is recorded in `seen` (by object id, other bundles' objects included): +the objects an object's documents reference, which the caller clears before converting an object. """ from __future__ import annotations @@ -25,6 +28,7 @@ "encode_object", "label", "message", + "omit", "ref_object_id", ] @@ -76,6 +80,21 @@ def encode_object(doc) -> bytes: return encode(_stored(doc), sort_keys=False) +def omit(tt: dict, *paths: str) -> dict: + """`tt` without the fields at `paths` (field names joined by "/"); the dictionaries on the way are copied.""" + out = dict(tt) + for path in paths: + head, _, rest = path.partition("/") + if head not in out: + continue + if rest: + if isinstance(out[head], dict): + out[head] = omit(out[head], rest) + else: + del out[head] + return out + + def ref_object_id(v) -> str | None: """The object id of a {"$ref": {...}} value (None for null or an unresolved file).""" if not isinstance(v, dict) or "$ref" not in v or "file" not in v["$ref"]: @@ -160,6 +179,7 @@ def __init__(self, view, fns: dict, *, scripts: dict, blob_min: int, png_level: self._heads: dict[int, tuple] = {} self._captures: dict[str, Capture] = {} self._local: Callable[[tuple], dict | None] | None = None + self.seen: set[str] = set() # ---------------------------------------------------------------- identities and names @staticmethod @@ -272,8 +292,9 @@ def script_class(self, key: tuple) -> str | None: # ---------------------------------------------------------------- references and values def dref(self, key: tuple) -> dict: - """{"$ref"} of an object, with its class and name when it is in the bundle.""" + """{"$ref"} of an object, with its class and name when it is in the bundle (recorded in `seen`).""" file, pid = key + self.seen.add(object_id(file, pid)) d = {"file": label(file), "pathId": pid} r = self.readers.get(key) if r is not None: diff --git a/src/nnnotes/unity/convert/prefab.py b/src/nnnotes/unity/convert/prefab.py index 0dd4e1c..845719e 100644 --- a/src/nnnotes/unity/convert/prefab.py +++ b/src/nnnotes/unity/convert/prefab.py @@ -145,7 +145,7 @@ def _component(ctx: BundleObjects, h: Hierarchies, o, root: tuple, unresolved: l def prefab(ctx: BundleObjects, info, h: Hierarchies, root: Root) -> None: - """`prefab`: {"name", "scene" (a root of a scene's file), "nodes"}: every node of the hierarchy in hierarchy + """`json`: {"name", "scene" (a root of a scene's file), "nodes"}: every node of the hierarchy in hierarchy order (path, name, path ids, active, layer, tag, local transform, rect, components with their fields).""" key = (info.file, info.path_id) g = h.graphs[info.file] @@ -190,4 +190,4 @@ def prefab(ctx: BundleObjects, info, h: Hierarchies, root: Root) -> None: else [] ) facts = {"nodes": len(nodes), "objects": len(root.members)} - ctx.emit(key, "prefab", encode_object(doc), "json", "prefab", "json", facts, issues=issues) + ctx.emit(key, "json", encode_object(doc), "json", "prefab", "json", facts, issues=issues) diff --git a/src/nnnotes/unity/convert/textures.py b/src/nnnotes/unity/convert/textures.py index 2cb7ec3..ed05194 100644 --- a/src/nnnotes/unity/convert/textures.py +++ b/src/nnnotes/unity/convert/textures.py @@ -2,6 +2,12 @@ A texture's image is decoded once per bundle: `texture` (or `decode_into`, when the task leaves textures out) puts it in a holder that the sprites cut from it read. Where a sprite's image comes from is its plan (`sprite_plan`). + +A sprite's `json` holds its typetree JSON (`sprite`), its decoded geometry (`mesh`: positions in the sprite's units, +triangles per submesh, texture coordinates when stored; a texture coordinate of a position p is +((p.x * uvTransform.x + uvTransform.y) / width, (p.y * uvTransform.z + uvTransform.w) / height) of the render data's +texture), the render data its image was cut from (`renderData`: the sprite's own or its atlas' entry for it) and +the image (`image`). """ from __future__ import annotations @@ -10,7 +16,7 @@ from ..atoms import Unsupported from ..ids import object_id -from .objects import BundleObjects, encode_object, message, ref_object_id +from .objects import BundleObjects, encode_object, message, omit, ref_object_id __all__ = [ "atlas_entries", @@ -24,13 +30,27 @@ ] +# the fields of a sprite's render data (SpriteRenderData, or a SpriteAtlas entry) that say how it is drawn +RENDER_FIELDS = ( + "texture", + "alphaTexture", + "secondaryTextures", + "textureRect", + "textureRectOffset", + "atlasRectOffset", + "uvTransform", + "downscaleMultiplier", + "settingsRaw", +) + + def _canon(v) -> str: return json.dumps(v, sort_keys=True, ensure_ascii=True) def texture(ctx: BundleObjects, info, holder: dict) -> None: - """`image`: PNG of the first mip level (HDR ASTC: the .astc file). The decoded image (or the error) goes into - `holder` for the texture's sprites.""" + """`image`: PNG of the first mip level (HDR ASTC: the .astc file); `json`: the typetree JSON without the pixels + (`image data`). The decoded image (or the error) goes into `holder` for the texture's sprites.""" from ..atoms.texture import ASTC_BLOCKS, format_name, is_hdr_astc key = (info.file, info.path_id) @@ -54,6 +74,7 @@ def texture(ctx: BundleObjects, info, holder: dict) -> None: ) data = ctx.fns["astc.container"](inp.data, w, h, ASTC_BLOCKS[fmt]) ctx.emit(key, "image", data, "astc", "texture.image", "astc", facts) + _texture_json(ctx, key, tt) return try: img = ctx.fns["texture.decode"](inp) @@ -64,6 +85,12 @@ def texture(ctx: BundleObjects, info, holder: dict) -> None: facts["mode"] = img.mode data = ctx.fns["png.encode"](img, ctx.png_level) ctx.emit(key, "image", data, "png", "texture.image", "png", facts) + _texture_json(ctx, key, tt) + + +def _texture_json(ctx: BundleObjects, key: tuple, tt: dict) -> None: + doc = ctx.tree(ctx.readers[key], omit(tt, "image data")) + ctx.emit(key, "json", encode_object(doc), "json", "unity.object", "json", {}) def decode_into(ctx: BundleObjects, info, holder: dict) -> None: @@ -145,8 +172,8 @@ def sprite_plan(ctx: BundleObjects, key: tuple, atlases: dict) -> dict: def sprite(ctx: BundleObjects, info, plan: dict, holder: dict) -> None: - """`image`: PNG on the sprite's rect cut from its texture's image in `holder`; `meta`: the sprite's JSON and - where its image came from.""" + """`image`: PNG on the sprite's rect cut from its texture's image in `holder`; `json`: the sprite's typetree + JSON, geometry, render data and image (see the module documentation).""" from ..atoms import sprite as sprite_atom err = holder.get("error") @@ -161,7 +188,8 @@ def sprite(ctx: BundleObjects, info, plan: dict, holder: dict) -> None: alpha = ctx.fns["texture.decode"](ctx.view.texture_input(ctx.info[plan["alpha"]])) if plan["alpha"] else None raw = int(src["settingsRaw"]) st = sprite_atom.settings(raw) - mesh = ctx.fns["mesh.arrays"](ctx.view.mesh_input(info)) if st.packing_mode == sprite_atom.TIGHT else None + m = ctx.fns["mesh.arrays"](ctx.view.mesh_input(info)) + mesh = m if st.packing_mode == sprite_atom.TIGHT else None downscale = float(src.get("downscaleMultiplier", 1.0)) img = ctx.fns["sprite.crop"]( holder["image"], @@ -179,30 +207,29 @@ def sprite(ctx: BundleObjects, info, plan: dict, holder: dict) -> None: refs.append({"rel": "atlas", "object": object_id(*plan["atlasKey"])}) facts = {"width": img.width, "height": img.height, "mode": img.mode} ctx.emit(key, "image", data, "png", "sprite.image", "png", facts, refs=refs) - meta = { + owner = plan["owner"] + doc = { "sprite": ctx.tree(r, tt), - "image": { + "mesh": sprite_atom.mesh_values(m) if "position" in m.channels else None, + "renderData": { "source": "atlas" if plan["atlasKey"] is not None else "renderData", "atlas": None if plan["atlasKey"] is None else ctx.dref(plan["atlasKey"]), - "texture": ctx.dref(plan["texture"]), - "alphaTexture": None if plan["alpha"] is None else ctx.dref(plan["alpha"]), - "textureRect": src["textureRect"], - "textureRectOffset": src["textureRectOffset"], - "settingsRaw": raw, + **{k: ctx.value(owner, src[k]) for k in RENDER_FIELDS if k in src}, + }, + "image": { "packed": st.packed, "packingMode": st.packing_mode, "packingRotation": st.packing_rotation, - "downscaleMultiplier": downscale, "width": img.width, "height": img.height, }, } - ctx.emit(key, "meta", encode_object(meta), "json", "sprite.meta", "json", {}, refs=refs) + ctx.emit(key, "json", encode_object(doc), "json", "sprite.meta", "json", {}, refs=refs) def sprite_external(ctx: BundleObjects, info, plan: dict, crop: str) -> None: - """`meta` of a sprite whose SpriteAtlas is in another bundle: its JSON, mesh and object facts, for the stage - `crop` that cuts its image from the atlas texture.""" + """`json` of a sprite whose SpriteAtlas is in another bundle: its typetree JSON, geometry and object facts, for + the stage `crop` that cuts its image from the atlas texture.""" from ..atoms import sprite as sprite_atom key = (info.file, info.path_id) @@ -217,7 +244,7 @@ def sprite_external(ctx: BundleObjects, info, plan: dict, crop: str) -> None: } facts = {"crop": crop, "atlas": atlas_oid} refs = [{"rel": "atlas", "object": atlas_oid}] - ctx.emit(key, "meta", encode_object(meta), "json", "sprite.meta", "json", facts, refs=refs) + ctx.emit(key, "json", encode_object(meta), "json", "sprite.meta", "json", facts, refs=refs) # ---------------------------------------------------------------- sprites cut from an atlas of another bundle @@ -233,7 +260,7 @@ def atlas_entry(entries: list, key): def crop_meta(meta: dict, entries: list, image, crop): - """The image of a sprite whose atlas is in another bundle, from its `meta` (sprite_external), its atlas' + """The image of a sprite whose atlas is in another bundle, from its `json` (sprite_external), its atlas' render data entries and `image(object id)`, the decoded image of an atlas texture; `crop`: atom sprite.crop.""" from ..atoms import sprite as sprite_atom diff --git a/tests/golden/unity.export.json b/tests/golden/unity.export.json index 96e8707..d310ee3 100644 --- a/tests/golden/unity.export.json +++ b/tests/golden/unity.export.json @@ -5,89 +5,89 @@ "clip.json": "clip.json/2", "controller.json": "controller.json/2", "cri.usm": "cri.usm/1", - "data": "data/1", - "font": "font/1", + "data": "data/2", + "font": "font/2", "generic.json": "generic.json/1", "gltf.write": "numpy-2.4.6/1", "material.json": "material.json/1", "mesh.arrays": "numpy-2.4.6/1", - "mesh.glb": "mesh.glb/1", + "mesh.glb": "mesh.glb/2", "mono.json": "mono.json/2", "png.encode": "pillow-12.3.0/1", - "prefab.json": "prefab.json/1", + "prefab.json": "prefab.json/2", "reader": "fakeunity/1", "scripts.json": "scripts.json/1", "shader.json": "shader.json/1", "sniff": "nnnotes/1", "sprite.crop": "pillow-12.3.0+numpy-2.4.6/1", - "sprite.png": "sprite.png/1", - "tex.png": "tex.png/1", + "sprite.png": "sprite.png/2", + "tex.png": "tex.png/2", "texture.decode": "unitypy-1.25.3+texture2ddecoder-1.0.6+astc-encoder-py-0.1.12+pillow-12.3.0/1" }, "fixtures": { "all": { - "key": "d3be85097dda7b39fb780c7b8d4560dbf88e85901372cc2256c2478b9d2ba1ef", - "result": "13f069ec2e00b913f537b8434f235b1232b5f875aebe1c3f40c39ff4e3274bdc" + "key": "10585d61586a4362e7e5a3c8857a9b583fac363abb5874e760d767fde9cb7d30", + "result": "3bd7b57ee922de132411cb8d027a00421fed5c5a595cdb3eef3e3cb5982b7374" }, "bundle.json": { - "key": "c81a9154d13d953e581bbd4cffe0590ad296a296ff6eeda9398aa8d394b44325", - "result": "cbaaf45561972f2004fa74352dc3adcc96c2baabadb56d85a49add792921fe9c" + "key": "fb99b80d44ff61bf4b90ea87e40a632c0f34372ddebd60dd00f74368c5f6b94d", + "result": "169b9bab48a8cb6423de878430b6291bf56999a5d640e9893dc2a1eeda9b93f9" }, "clip.json": { - "key": "07f85633bae3127fe11950188284b9325066133b4d86eaf61c3414b3c622fa77", - "result": "f133a83bce862cef316e2ea2907790b073eb7928911150bed8225529534df177" + "key": "535034c7ce7875a3ea19ebadbb4d12bca912621d38982cd47097cb1c66ff62c5", + "result": "fad3849534a5ad3892f968f567c88efa426088232df6e26a8f646757592fa135" }, "controller.json": { - "key": "0488d21f6759dc95639a3d1e643980472452fbd4b04c3d5623942a1095b5e4a5", - "result": "c1ca4ec4b97bb18b24a8effc3a0faa4f5dc5a6e3724eeba8368dd0505f59235a" + "key": "522f6cefe4102041b197c0feda95de6668a7e88ab548b0d682ef26b492079fd1", + "result": "f17bc2432ad36b87f17e3fffec59eab429a803d23f0bc63db018ba2d99a847db" }, "cri.usm": { "key": "8d1d45652a6a631253b3bad7f096af988fcc3894831a8fe5e7ec90b939bc3427", - "result": "ca67e2f4878b562b24cb6ca374c469ee2085a2c0259245adcae05cba4f9095b1" + "result": "9a0ca28ce190fc8b5c03fc108b657825c1361a05b416894d2f9c55402cae62fd" }, "data": { - "key": "62b78ee012b053bfb1ee11245b07cc58428f13bd0fb1b5de221b7b0a22372654", - "result": "7ca1098d52be75092183689a236f8d0b876edd68684e32b691364bad93ca89bd" + "key": "98f3c28baedd007e88d9a48adccdad2a1970e63df6e487917cf1c6823feb651d", + "result": "a2c0972bc1c388038dedb9b70f4a8a6d67b58dfca0faac3c438610df125c7234" }, "font": { - "key": "7b6638e13d5a7771ca82970c82ff10e8e815e9ae11e022f608829ca7e6ff2a11", - "result": "bdbbe3b89be967d95eb2cfb1e116773b149d4ba521551807030627e8d57c5cca" + "key": "c1a50c8ae58a104313d93a7296464812f98c82d8c9aa516589b9d6e5cb89bffa", + "result": "d18d119de09ed000abca438357e72951a18d576c2b26b76932adf9621f67d2bd" }, "generic.json": { - "key": "56d22e25eec789a5b67a0e73ccd1db2edc9a1b50f23e7bc367ae26e7c1410509", - "result": "f404a37e961ab0a92b773317a65eff12caa0a2a8816d5f954a329bb967d1d638" + "key": "3f111bf015cc23af61fb457ad94be8c3479259d2e8ec2388390580161179afde", + "result": "a79ea2404c179705d54adb9c08f917acedd070bffccc01ba72da3f68af522cd4" }, "material.json": { - "key": "413ce336e41f3966b370858050aa0ba522b35862338b6762d78747b9d36940c5", - "result": "246599600a2de4bae74b7b13dbc83ebf7856e1d13a1e1ee35b8274706eae2068" + "key": "fd0603efb48c4582fa8664efcc44be7e238dc6168846dd5528ac6ec350732903", + "result": "373545644b49d9047bae346a78186d7bd0b1b5158d17a393b42050e505209e32" }, "mesh.glb": { - "key": "d73226629446825538e9f52f7139470cc2ee3d06480cbf480d8cb8876b98e491", - "result": "bb4ce49483164483c3729736c92f82e5b48db83f4ed67c57890a30b976ec1ed0" + "key": "d14c16a5c18145cc039a4568b4c8561b0120644c2de1e1abd1dd1be1f17917f0", + "result": "650489a1a9d4e52ae66bc44a101ad5c127e9aa836c8d69e4c08c662e70c73b47" }, "mono.json": { - "key": "3da938e46ad6126b00d2b4efd67416bbc3b7983dfc39ec18a92da5dae1cbd5d6", - "result": "c20cf16802c538e51cd327facc43a3401da0ca67bf5f1adaa7a3554ff1d56954" + "key": "5820ec6ca51a697d9ad2f3ed1ee5c8a849fd8f813b053f335b86b1b95082abd8", + "result": "cdabce4d56148f552c87738a9208fd36d0cc581590d7e5eb92f8e5b522de2266" }, "prefab.json": { - "key": "c0e4016cd8ccad7ef8870c61a8a4be8271c2997683b4d868aa99a9763a5bdee9", - "result": "7512a5b00635e586c8a0e8777897b7f2287a293f39552af78a6ab8d8d6c65b69" + "key": "8994f1ef92388d443797f11c65a61a6346acf01ff0dc8bfbd36dc2fb2d2d23d2", + "result": "368986bcb1df65ea7fb906e01fe49b814c35e41212a079cdfcc5b70d5ed06559" }, "scripts.json": { - "key": "82609d6edbc52764a5f491b6abe2e8bf1f1383336549058d588dbed6ca72f1fe", - "result": "48743bb2322d8e81d31607cab08986e7e6b7b921543b25067fac0deb875d0478" + "key": "aafda4c0fe111458f3b48e85b8f60d0e9234dd250590591966a8ac3305d99ef9", + "result": "a2644c8fe5f8198b3771374f10ad01b91a9b111d74aff79b85a9b9b2e071e2c5" }, "shader.json": { - "key": "355da22724a98e77ccf12c9f7d532fd6e3c8cd23fd34ea4d3c536136a36c2bca", - "result": "5404f4143a35b8efd83ac2f09f9ab2c4c52782db76c4c9c3637afa6b7d4bc2fb" + "key": "a71aa982ce0da9929eaf721a1db903ac0565e1232c94a1d451cdf818f2c59f31", + "result": "fef75615b20f6dc056a4934606d1f094159e219b87482b5efae95c407d0f6d55" }, "sprite.png": { - "key": "0ed6598b9a398b409e57f47465f5b7f5106a4ea7f6327771c6272fa4e3df4617", - "result": "50a216c09f9a3479eacd619bc857b27ee84345ac251d5b73c0a0493246646aae" + "key": "d42d38c45711b2de94fc4509b0dd8d919d01d123091f09d54e177df7e4cf751d", + "result": "5c6d4968cbc56a1eefb9c8c94b380487f9876f591e33acbef40bb735edbb70b9" }, "tex.png": { - "key": "03e26506c9a6187554a6047f78e9ecded3c470632550fee0067cc992f6823077", - "result": "9ef852eca9d54b96138aab99e835507d7c27f25d921b2741610d0cd0f18f9d8c" + "key": "a5af40387bb2c9fe6ba31415af4e4028163eab23bd4114ad4384b57159ae1318", + "result": "54d91ffc00400f162f677f9e1525e34698b33dc439560b4372dffcd09a8dd73b" } }, "libraries": { diff --git a/tests/pipeline/test_layout.py b/tests/pipeline/test_layout.py index b4a8655..7ef6086 100644 --- a/tests/pipeline/test_layout.py +++ b/tests/pipeline/test_layout.py @@ -72,7 +72,7 @@ def test_objects_of_one_container(tmp_path): "Sprite", "square", "assets/x/member_full.png", - {"image": png("s1"), "meta": (b"{}", "json")}, + {"image": png("s1"), "json": (b"{}", "json"), "meta": (b"[]", "json")}, ), ("CAB-1", 12, "Sprite", "member_full", "assets/x/member_full.png", {"image": png("s2")}), ("CAB-1", 21, "Sprite", "dup", "assets/x/member_full.png", {"image": png("d1")}), @@ -83,7 +83,7 @@ def test_objects_of_one_container(tmp_path): "GameObject", "foo", "assets/p/foo.prefab", - {"prefab": (b"{}\n", "json"), "blob:m_Data/Array": (b"MOC3", "moc3")}, + {"json": (b"{}\n", "json"), "blob:m_Data/Array": (b"MOC3", "moc3")}, ), ("CAB-1", 40, "Texture2D", "hdr", "assets/x/hdr.png", {"image": (b"\x13\xab\xa1\x5c", "astc")}), ], @@ -92,11 +92,12 @@ def test_objects_of_one_container(tmp_path): assert p == { "CAB-1:10#image": "assets/x/member_full.png", "CAB-1:11#image": "assets/x/member_full[square].png", + "CAB-1:11#json": "assets/x/member_full[square].png.json", "CAB-1:11#meta": "assets/x/member_full[square].png.meta.json", "CAB-1:12#image": "assets/x/member_full[member_full].png", "CAB-1:21#image": "assets/x/member_full[dup]~21.png", "CAB-1:22#image": "assets/x/member_full[dup]~22.png", - "CAB-1:30#prefab": "assets/p/foo.prefab.json", + "CAB-1:30#json": "assets/p/foo.prefab.json", "CAB-1:30#blob:m_Data/Array": "assets/p/foo.prefab.blob.m_Data.Array.moc3", "CAB-1:40#image": "assets/x/hdr.png.astc", } diff --git a/tests/pipeline/test_objexport.py b/tests/pipeline/test_objexport.py index 79cab58..ddee38c 100644 --- a/tests/pipeline/test_objexport.py +++ b/tests/pipeline/test_objexport.py @@ -669,7 +669,7 @@ def test_every_object_has_exactly_one_item(exported): oid(74): ("exported", None), oid(51): ("exported", None), } - assert items[oid(61)]["in"] == f"{oid(60)}#prefab" and items[oid(80)]["in"] == "unity.export:all#scripts.json" + assert items[oid(61)]["in"] == f"{oid(60)}#json" and items[oid(80)]["in"] == "unity.export:all#scripts.json" assert items[oid(1)]["in"] == "unity.export:all#bundle.json" assert items[oid(94)]["artifacts"] == [f"{oid(94)}#json"] assert doc["status"] == "ok" and all(i["class"] for i in doc["items"]) @@ -702,7 +702,7 @@ def test_provenance_facts_and_atoms(exported): assert a["provenance"]["inputs"] == [{"role": "bundle", "sha256": task.inputs[0].sha256}] g = art(doc, f"{oid(95)}#json") assert set(g["provenance"]["atoms"]) == {"reader", "sniff", "generic.json"} - assert task.atoms["tex.png"] == "tex.png/1" and task.atoms["reader"] == "fakeunity/1" + assert task.atoms["tex.png"] == "tex.png/2" and task.atoms["reader"] == "fakeunity/1" # ---------------------------------------------------------------- converters @@ -756,11 +756,27 @@ def test_sprites_same_bundle(exported): canvas={"rect": tt["m_Rect"], "offset": src["textureRectOffset"], "pivot": tt["m_Pivot"]}, ) assert load(depot, doc, f"{oid(pid)}#image") == png.encode(want) - meta = obj(depot, doc, f"{oid(pid)}#meta") - assert meta["image"]["texture"] == {"$ref": {"file": CAB, "pathId": 10, "class": "Texture2D", "name": "tex"}} + meta = obj(depot, doc, f"{oid(pid)}#json") + rd = meta["renderData"] + assert rd["texture"] == {"$ref": {"file": CAB, "pathId": 10, "class": "Texture2D", "name": "tex"}} + assert rd["textureRect"] == src["textureRect"] and rd["uvTransform"] == src["uvTransform"] + assert rd["settingsRaw"] == src["settingsRaw"] and rd["alphaTexture"] is None assert meta["sprite"]["m_Name"] == tt["m_Name"] - meta = obj(depot, doc, f"{oid(12)}#meta") - assert meta["image"]["source"] == "atlas" and meta["image"]["atlas"]["$ref"]["pathId"] == 20 + pos, idx = _sprite_mesh(tt["m_Rect"]) + assert meta["mesh"]["positions"] == np.asarray(pos, np.float32).astype(float).tolist() + assert meta["mesh"]["submeshes"] == [np.asarray(idx).reshape(-1, 3).tolist()] + assert meta["image"] == { + "packed": bool(src["settingsRaw"] & 1), + "packingMode": (src["settingsRaw"] >> 1) & 1, + "packingRotation": (src["settingsRaw"] >> 2) & 0xF, + "width": int(tt["m_Rect"]["width"]), + "height": int(tt["m_Rect"]["height"]), + } + assert by_object(doc)[oid(pid)]["refs"] == [oid(10)] + ([oid(20)] if pid == 12 else []) + meta = obj(depot, doc, f"{oid(11)}#json") + assert meta["renderData"]["source"] == "renderData" and meta["renderData"]["atlas"] is None + meta = obj(depot, doc, f"{oid(12)}#json") + assert meta["renderData"]["source"] == "atlas" and meta["renderData"]["atlas"]["$ref"]["pathId"] == 20 assert meta["sprite"]["m_RD"]["m_VertexData"]["m_DataSize"]["$hex"] # small: inline refs = art(doc, f"{oid(12)}#image")["semantics"]["refs"] assert refs == [{"rel": "texture", "object": oid(10)}, {"rel": "atlas", "object": oid(20)}] @@ -768,8 +784,8 @@ def test_sprites_same_bundle(exported): def test_sprite_with_its_atlas_elsewhere_gets_a_meta_only(exported): depot, _task, doc = exported - assert by_object(doc)[oid(13)]["artifacts"] == [f"{oid(13)}#meta"] - a = art(doc, f"{oid(13)}#meta") + assert by_object(doc)[oid(13)]["artifacts"] == [f"{oid(13)}#json"] + a = art(doc, f"{oid(13)}#json") assert a["semantics"]["facts"] == {"crop": CROP, "atlas": f"{ATLAS_CAB}:99"} meta = obj(depot, doc, a["id"]) assert meta["image"] == {"source": "atlas", "atlas": {"$ref": {"file": ATLAS_CAB, "pathId": 99}}, "stage": CROP} @@ -801,9 +817,48 @@ def test_text_and_fonts(exported): assert load(depot, doc, f["id"]) == _ttf() and f["content"]["ext"] == "ttf" +def test_every_converted_object_has_its_json(exported): + depot, _task, doc = exported + tex = obj(depot, doc, f"{oid(10)}#json") + assert "image data" not in tex and tex["m_Width"] == W and tex["m_TextureFormat"] == 4 + assert art(doc, f"{oid(10)}#json")["semantics"] == {"kind": "unity.object", "format": "json", "facts": {}} + assert obj(depot, doc, f"{oid(15)}#json")["m_MipCount"] == 2 # HDR ASTC: the .astc image and the json + mesh = obj(depot, doc, f"{oid(30)}#json") + assert "m_DataSize" not in mesh["m_VertexData"] and "m_IndexBuffer" not in mesh + assert mesh["m_VertexData"]["m_VertexCount"] == 3 and mesh["m_Name"] == "tri" + text = obj(depot, doc, f"{oid(40)}#json") + assert text == {"m_Name": "readme"} + font = obj(depot, doc, f"{oid(42)}#json") + assert font == {"m_Name": "font"} + items = by_object(doc) + for pid in (10, 11, 12, 15, 30, 40, 41, 42, 44, 45, 50, 60, 70, 73, 74, 90, 92, 95, 96): + assert f"{oid(pid)}#json" in items[oid(pid)]["artifacts"], pid + assert all( + i["status"] != "exported" or any(a.endswith("#json") for a in i["artifacts"]) or i["object"] == oid(13) + for i in doc["items"] + if i["class"] not in ("AssetBundle", "MonoScript") + ) + + +def test_items_name_the_objects_they_reference(exported): + _depot, _task, doc = exported + items = by_object(doc) + assert items[oid(50)]["refs"] == [oid(10), f"{ATLAS_CAB}:5"] # the material's shader and texture + # a prefab: the objects its components reference outside it, other bundles' included; its own objects are named + # by their paths + assert items[oid(60)]["refs"] == [oid(10), "CAB-scripts:3", "CAB-scripts:7", "CAB-scripts:8"] + assert "refs" not in items[oid(61)] and "refs" not in items[oid(63)] # contained in the prefab + assert items[oid(1)]["refs"] == [oid(10), oid(11), oid(40), oid(60)] # the AssetBundle's container + assert items[oid(92)]["refs"] == [oid(90)] # the controller's clip + assert items[oid(96)]["refs"] == ["CAB-scripts:12"] # a clip binding's script + assert items[oid(13)]["refs"] == [f"{ATLAS_CAB}:99"] # a sprite's atlas in another bundle + assert items[oid(70)]["refs"] == [oid(80)] # its script + assert "refs" not in items[oid(30)] and "refs" not in items[oid(40)] + + def test_prefab(exported): depot, _task, doc = exported - p = obj(depot, doc, f"{oid(60)}#prefab") + p = obj(depot, doc, f"{oid(60)}#json") assert p["name"] == "root" and [n["path"] for n in p["nodes"]] == ["root", "root/child"] root, child = p["nodes"] assert root["pathId"] == 60 and root["transformPathId"] == 61 and root["layer"] == 5 @@ -814,9 +869,9 @@ def test_prefab(exported): assert fields["tex"] == {"$ref": {"file": CAB, "pathId": 10, "class": "Texture2D", "name": "tex"}} assert fields["m_Script"] == {"$ref": {"file": "CAB-scripts", "pathId": 7}} assert math.isnan(fields["value"]) and fields["small"] == {"$hex": "616263"} - assert b'{"$float": "nan"}' in load(depot, doc, f"{oid(60)}#prefab").replace(b"\n", b"").replace( + assert b'{"$float": "nan"}' in load(depot, doc, f"{oid(60)}#json").replace(b"\n", b"").replace( b" ", b"" - ) or b'"$float"' in load(depot, doc, f"{oid(60)}#prefab") + ) or b'"$float"' in load(depot, doc, f"{oid(60)}#json") (c,) = child["components"] assert c["fields"]["up"] == {"transform": "root"} assert c["fields"]["comp"] == {"component": "MonoBehaviour", "gameObject": "root", "class": "Behaviour"}