From a4c16e169b61378d03916046f7b9817c84d55030 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 15 Oct 2026 14:52:32 +0800 Subject: [PATCH 1/4] feat(pipeline)!: name built-in objects by class as product roots The address table (nnnotes.addresses/3) gains a `builtins` table: the objects of the APK's built-in files by class ("GraphicsSettings") and MonoBehaviours by their script's class ("MonoBehaviour:"), without MonoScripts and with a hierarchy listed by its root only. AddressIndex.builtin(selector) reads it. A product stage can name these selectors as roots (ProductStage.builtins); the request fact "roots" carries them, so the units of settings assets that the game reads without a catalog key or a Resources.Load path are exported with the products that use them. BREAKING CHANGE: link.addresses is at version 3 and writes nnnotes.addresses/3; the roots fact has a "builtins" list. Co-Authored-By: Claude Opus 5.5 --- docs/contracts.md | 2 +- docs/products.md | 11 +- docs/schemas.md | 2 +- docs/stages.md | 8 +- src/nnnotes/pipeline/assemble.py | 28 +- src/nnnotes/pipeline/link.py | 41 +- src/nnnotes/pipeline/units.py | 28 +- src/nnnotes/pipeline/views.py | 36 +- src/nnnotes/schemas/addresses.schema.json | 18 +- tests/pipeline/test_assemble.py | 48 +- tests/pipeline/test_link.py | 41 + tests/pipeline/test_views.py | 7 +- uv.lock | 2135 +++++++++++++++++++++ 13 files changed, 2356 insertions(+), 49 deletions(-) create mode 100644 uv.lock diff --git a/docs/contracts.md b/docs/contracts.md index 7c63b3c..d23c9e9 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -229,7 +229,7 @@ Documents written by particular stages (artifacts of no object: their ids are `< | `nnnotes.catalog-index/1` | `catalog.index` | [catalog-index.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/catalog-index.schema.json) | | `nnnotes.census/2` | `unity.census` | [census.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/census.schema.json) | | `nnnotes.scripts/1` | `link.scripts` | [scripts.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/scripts.schema.json) | -| `nnnotes.addresses/2` | `link.addresses` | [addresses.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/addresses.schema.json) | +| `nnnotes.addresses/3` | `link.addresses` | [addresses.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/addresses.schema.json) | | `nnnotes.view/1` | `view.` | [view.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/view.schema.json) | | `nnnotes.objects/1` | `pipeline.assemble` (a product's object table) | [objects.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/objects.schema.json) | diff --git a/docs/products.md b/docs/products.md index 366e81d..10b4573 100644 --- a/docs/products.md +++ b/docs/products.md @@ -14,12 +14,15 @@ objects: `link.addresses`; - the objects of the Resources.Load paths it gives (`resources`): paths of the ResourceManager in the APK's built-in files, such as the materials a runtime loads by name, through the same table and compared ignoring case; +- the objects of the built-in selectors it gives (`builtins`): objects of the APK's built-in files by class + (`GraphicsSettings`) or a MonoBehaviour by its script's class (`MonoBehaviour:`), such as settings assets + the game reads without a path, through the same table; - the object ids the stage names besides (`root_objects`). All are read from the planning facts alone, so a command can ask for what its products need before anything runs: -`assemble.request(stages, products, env)` gives the fact `roots` (`{"keys", "resources", "objects"}`), and the request of -`unity.export` then holds the units of those objects and every unit reachable from them through the census `refs` -([units](stages.md#units)), and nothing else of their bundles. +`assemble.request(stages, products, env)` gives the fact `roots` (`{"keys", "resources", "builtins", "objects"}`), and +the request of `unity.export` then holds the units of those objects and every unit reachable from them through the +census `refs` ([units](stages.md#units)), and nothing else of their bundles. What a task reaches: the units holding its roots, then every unit that an item of a reached unit references (the item's `refs`), through the `unity.export` results. A sprite whose atlas is in another bundle has its `image` from the @@ -29,7 +32,7 @@ object that a reached object references and no census holds is listed as `missin | Part of the task | | |---|---| | inputs | `objects`: the object table of what it reaches (`nnnotes.objects/1`); the stage's own inputs (master tables, data its parameters name) | -| depends | `link.addresses` (when it has keys or resource paths), the `unity.export` and `sprite.crop` tasks whose results it read | +| depends | `link.addresses` (when it has keys, resource paths or built-in selectors), the `unity.export` and `sprite.crop` tasks whose results it read | | artifacts | `#`: the product's own files; `#objects`: the object table of the objects its files use (the whole table unless the stage narrows it to some objects, or to some files of an object: a shader's programs for one graphics API) | | item | one for the subject: `exported`, or `unsupported` with a reason code | diff --git a/docs/schemas.md b/docs/schemas.md index 6ffa241..fa3c1f9 100644 --- a/docs/schemas.md +++ b/docs/schemas.md @@ -14,7 +14,7 @@ schemas.load("plan") # the schema as a dict | Schema | Format | Document | |---|---|---| -| `addresses` | `nnnotes.addresses/2` | the address table of a catalog version and the censuses of its bundles | +| `addresses` | `nnnotes.addresses/3` | the address table of a catalog version and the censuses of its bundles | | `artifact` | `nnnotes.artifact/1` | the content, provenance and semantics of one output file | | `catalog-diff` | `nnnotes.catalog-diff/1` | the difference between two catalog versions (`catalog diff`) | | `catalog-index` | `nnnotes.catalog-index/1` | the index of a catalog pair: remote and APK catalogs, every location in catalog order | diff --git a/docs/stages.md b/docs/stages.md index 6e26c6f..661a8d5 100644 --- a/docs/stages.md +++ b/docs/stages.md @@ -13,8 +13,8 @@ The asset export is built from three layers: ## Stages Every stage is at version 1, except `unity.census` (version 4: it lists what every object references, the -hierarchy holding it and the Resources.Load paths of a ResourceManager), `link.addresses` (version 2: the -Resources.Load paths), `live2d.model` (version 2: assembled from the exported objects), `unity.export` (version 2: one task per unit), `cri.movie` (version 2: every stream kind, +hierarchy holding it and the Resources.Load paths of a ResourceManager), `link.addresses` (version 3: the +Resources.Load paths and the built-in selectors), `live2d.model` (version 2: assembled from the exported objects), `unity.export` (version 2: one task per unit), `cri.movie` (version 2: every stream kind, codec and channel of a USM) and `catalog.index` (version 2: gzip input and Japanese remote asset placeholders). | Stage | Subject | Inputs | Outputs | @@ -29,7 +29,7 @@ codec and channel of a USM) and `catalog.index` (version 2: gzip input and Japan | `live2d.model`, `live2d.cubism` | a Live2D model key (`Character/Live2D///model/`) a command asks for | `objects`: the object table of what the model reaches ([products](products.md#live2d-models)); depends on `link.addresses` and the `unity.export` tasks it reads | `model.json` (the player's model) and the object table; the Cubism SDK files | | `room.gltf` | a spot background key (`Spot//Background/`) | `objects`: the object table of what the background reaches ([products](products.md#spots)) | `room.glb` and `room.json` | | `spot.files` | a spot situation key (`Spot///`) | `objects`; `master`: the spot's master record | `spot.json` and the Spine files | -| `link.addresses` | global | catalog index, censuses (the built-in files' too) | key -> objects, Resources.Load path -> objects, serialized file -> bundle or built-in file, bundle -> keys | +| `link.addresses` | global | catalog index, censuses (the built-in files' too) | key -> objects, Resources.Load path -> objects, built-in selector (class, MonoBehaviour script class) -> objects, serialized file -> bundle or built-in file, bundle -> keys | | `view.` | a catalog (`main`) | `master:` per master table it reads, `rules` (the view's rule and the resolvers it uses), `addresses` (the part of the address table it read: the keys looked up and the prefixes listed); no parameters, atoms or context | `view.:#view` (facts: rows, entries, gaps, unreferenced) | | `view.voices` | a catalog (`main`) | `master:
` per master table its rules read, `rules`, `addresses`, `sheets` (each cue sheet the `cri.audio` results are known by that a voice names or the rules' prefixes list: its task and ACB content id), `acb:` (the ACB of each decoded one), `episodes` (per story episode and part, the `unity.export` artifact holding the object its key names), `adv:` (those contents); context `audio` (the keys of those `cri.audio` results); runs after `unity.export` and `cri.audio` | `view.voices:#view` (facts: rows, entries, gaps, unreferenced streams) | @@ -112,7 +112,7 @@ several serialized files the file's tag follows the bundle name, as in `link.add bundle's content changes, so a unit keeps its task across catalog versions. A run exports the units holding an object of the selected bundles, the units of the objects the fact "roots" names -(catalog keys, Resources.Load paths and object ids: a product's roots, [products](products.md)) and every unit reachable +(catalog keys, Resources.Load paths, built-in selectors and object ids: a product's roots, [products](products.md)) and every unit reachable from those through what their objects reference (the census `refs`, across the censuses of the run); with `--only-class` only the units whose root object is of those classes. The selection is not part of any key: a unit's task is the same whichever request reaches it. diff --git a/src/nnnotes/pipeline/assemble.py b/src/nnnotes/pipeline/assemble.py index 9ba12f6..94cdc44 100644 --- a/src/nnnotes/pipeline/assemble.py +++ b/src/nnnotes/pipeline/assemble.py @@ -1,7 +1,8 @@ """Product stages: products assembled from the unity.export results of the objects they reach, never from a bundle. A product (a Live2D model, a story, a live, ...) is a stage whose task per subject starts from root objects: the -objects of the catalog keys `keys(subject)` gives, of the Resources.Load paths `resources(subject)` gives and the +objects of the catalog keys `keys(subject)` gives, of the Resources.Load paths `resources(subject)` gives, of the +built-in selectors `builtins(subject)` gives (objects of the APK's built-in files by class or script class) and the objects `root_objects(subject)` names (all read from the planning facts alone, so a command can ask for the units its products need before anything runs: `request`). The objects it reaches are those of the units holding its roots and every unit an item of a reached unit references @@ -36,15 +37,21 @@ def request(stages: dict, products: dict, env) -> dict: """The fact "roots" (pipeline.units) of a request for `products` {stage name: subjects}: the catalog keys, the - resource paths and the root objects of every subject, sorted.""" - keys, resources, objects = set(), set(), set() + resource paths, the built-in selectors and the root objects of every subject, sorted.""" + keys, resources, builtins, objects = set(), set(), set(), set() for name, subjects in sorted(products.items()): stage = stages[name] for s in subjects: keys.update(stage.keys(s, env)) resources.update(stage.resources(s, env)) + builtins.update(stage.builtins(s, env)) objects.update(stage.root_objects(s, env)) - return {"keys": sorted(keys), "resources": sorted(resources), "objects": sorted(objects)} + return { + "keys": sorted(keys), + "resources": sorted(resources), + "builtins": sorted(builtins), + "objects": sorted(objects), + } def _entry(item: dict, arts: dict) -> dict: @@ -271,6 +278,11 @@ def resources(self, subject: str, env) -> list[str]: """The Resources.Load paths whose objects are roots of `subject` (from the facts alone).""" return [] + def builtins(self, subject: str, env) -> list[str]: + """The built-in selectors ("", "MonoBehaviour: