Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,11 @@ the APK's UI prefabs -> an offline UI prefab library for ournotes-player/ui
| `--key` `KEY` | exact APK catalog key (repeatable) |
| `--prefix` `PREFIX` | APK UI key prefix (default EmbUI/) |
| `--limit` `LIMIT` | maximum number of keys |
| `--no-dependencies` | omit additional prefab roots and controllers |
| `--force` | re-export the selected keys |
| `--stored-catalog` `LABEL\|SHA` | a catalog version of the depot (`catalog versions`; default: the current catalog) |
| `--workers` `N` | worker processes (default: the usable CPUs) |
| `--link` `LINK` | as export assets --link (one of `auto`, `clone`, `hard`, `copy`) |
| `--explain` | print every setting of the run first |
| `--dry-run` | print the plan, run nothing |

## nnnotes apk settings

Expand Down
7 changes: 7 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,10 @@ of them. Downloads, file identities by path (`inputs/paths/`), catalog versions,
stay in the depot directory.

Every request names one object; only `depot gc --remote` lists the remote.

## UI contracts

`nnnotes.ui-pack/1` describes a UI product's root objects and animation bindings. `nnnotes.ui-library/1` indexes its selected keys, referenced prefabs and controllers. Both use the shared `nnnotes.product/1` object storage contract; see [UI libraries](ui.md).

- [ui-pack.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/ui-pack.schema.json)
- [ui-library.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/ui-library.schema.json)
6 changes: 6 additions & 0 deletions docs/products.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,9 @@ asks for the `story` product first, reads its `story.json`, then asks for the st
`parts.ui` and `parts.fonts` map every client language to its product manifest. The command exports those entries
for the requested languages. The `product` layout makes the story of
every episode whose key the selection holds.

## UI library products

`ui.pack` assembles one catalog key and its reachable objects. Its `ui.json` (`nnnotes.ui-pack/1`) names root prefabs, controllers, animation bindings and shaders. The product layout writes `index.json` (`nnnotes.ui-library/1`) over the selected UI products; all referenced data lives in their object tables. See [UI libraries](ui.md).

A stage with `INCLUDE_FONTS = True` requests traversal through its referenced font objects. The request records these roots in `font_roots`; unit selection and assembly follow the same rule. Other products keep their own font selection behavior.
2 changes: 2 additions & 0 deletions docs/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ schemas.load("plan") # the schema as a dict
| `story-host` | `nnnotes.story-host/1` (`format`) | an Overlay story's host screen, layout and initial state |
| `story-simple-ui` | `nnnotes.story-simple-ui/1` (`format`) | localized Overlay talk-window and system-message prefabs |
| `task` | `nnnotes.task/1` | one stage applied to one subject, with everything needed to run it (`depot run`) |
| `ui-library` | `nnnotes.ui-library/1` | selected UI keys and their product manifests |
| `ui-pack` | `nnnotes.ui-pack/1` | native UI roots, prefab and controller identities, animation bindings |
| `view` | `nnnotes.view/1` | a semantic view of a master table, each row with its names and the objects it names |

The documents of the asset export are described in [contracts](contracts.md), the views in [views](views.md). The
Expand Down
4 changes: 4 additions & 0 deletions docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,7 @@ The codes of items that are `generic`, `unsupported` or `failed`, and of limits
| `failed.export` | the object's typetree JSON could not be made |
| `source.absent` | a catalog entry whose file is not there (an APK entry without a file) |
| `source.error` | an input that could not be fetched |

## ui.pack

`ui.pack` assembles the selected APK UI key from its object table, including referenced font objects. It writes `ui.json` with native roots, prefab/controller lists and named animation bindings. The product layout groups its manifests into the UI library index. See [UI libraries](ui.md).
83 changes: 31 additions & 52 deletions docs/ui.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,45 @@
# Serialized UI libraries (experimental)
# UI libraries

`nnnotes ui` exports an offline prefab library for the optional `ournotes-player/ui` preview module. It reads the
embedded Addressables catalog and bundle closure of the APK set you supply; it does not contact a game API. Keep
outputs outside the nnnotes and ournotes-player source repositories. The export contains game data that these
repositories and their packages do not redistribute.
`nnnotes export ui` assembles an offline UI library for `ournotes-player/ui`. The supplied APK set provides the
catalog keys, native prefab fields, animations, sprites and fonts. The depot stores their converted objects and
product results; the output directory is a product layout.

```sh
nnnotes --apk /path/to/base.apk ui --prefix EmbUI/Prefab/ --limit 10 -o /path/to/ui-data
nnnotes --apk /path/to/base.apk ui --key YOUR_EXACT_APK_KEY -o /path/to/ui-data
nnnotes --apk /path/to/base.apk export ui --prefix EmbUI/Prefab/ --limit 10 -o /path/to/ui-data
nnnotes --apk /path/to/base.apk export ui --key YOUR_EXACT_APK_KEY -o /path/to/ui-data
```

`--key` is repeatable. Without it, `--prefix` defaults to `EmbUI/`. `--no-dependencies` omits additional root
prefabs and AnimatorControllers; `--force` re-exports the selected keys. A failed key is retained as an explicit
index entry and makes the command exit with status 1. Existing unrelated entries of the same input stay present.
`--key` is repeatable. Otherwise `--prefix` selects APK asset keys and defaults to `EmbUI/`; `--limit` takes the
first keys in sorted order. `--stored-catalog` selects a saved catalog version. The common `--workers`, `--link`,
`--dry-run` and `--explain` options apply. The output describes the complete selection of that invocation;
repeated invocations reuse unchanged depot tasks. Reports under `_reports/` describe coverage and failures.

## Inputs and regions
## Input configuration

The command uses the same `ApkSet` reader as the existing extractors: one APK, base.apk with adjacent splits, a
directory of splits, or an APKS/XAPK archive. Explicit `--apk` wins over the selected region's APK configuration.
The configured depot and bundle decryption settings are used when needed. Readable unencrypted local bundles do
not require a key. Default Unity resources are read from the selected APK set through the existing player reader.
Configure the APK set and depot as described in [configuration](configuration.md). APK input may be a single APK,
base.apk with adjacent splits, a split directory, or an APKS/XAPK archive. Region selection chooses its configured
APK. Keep each library's output directory associated with its intended input configuration.

```sh
nnnotes --region jp ui --prefix EmbUI/Prefab/ --limit 10 -o /path/to/jp-ui-data
```

For JP, configure the actual JP APK set as described in [jp.md](jp.md). Do not point a JP region at an international
APK: this command exports the APK selected by the configuration, not a replacement fetched from that region.
Keep international and JP libraries in separate output directories. Their keys, classes and fonts can differ.

Current real UI smoke checks used international client 1.0.1 (versionCode 25). A synthetic split-APK test verifies
the JP command/configuration path; it is not a full JP UI export or pixel-fidelity verification. Existing JP
catalog, chart, story and model validation is described separately in [jp.md](jp.md).

## Files and resume

`index.json` has `schema: 1`, `format: "ournotes-ui-library"`, and three lists:
## Product layout

- `assets`: selected catalog keys, IDs, kind, export status, pack file and SHA-256; failures contain an error type
and message rather than a pack file.
- `embedded`: additional root GameObjects found in a selected key's dependency closure.
- `controllers`: AnimatorControllers found in that closure, keyed by actual serialized object identity.
`index.json` has `format: "nnnotes.ui-library/1"`:

Pack files under `packs/`, `embedded/` and `controllers/` have `format: "ournotes-ui-pack"`, a `document`,
`resources` and `resourceBase: "../"`. Hierarchy nodes retain serialized rectangles, transforms, active flags,
component fields and preorder paths. `nodeId` and reference `nodeId` values distinguish same-named instances;
controller references carry a matching ID. Negative intermediate rectangle sizes are preserved.
- `assets`: selected catalog keys, display names, stable key IDs, root object IDs and product manifest paths.
- `embedded`: referenced prefab roots, identified by serialized object ID.
- `controllers`: referenced AnimatorController objects, with their object IDs and containing product manifests.

Resources contain Sprite metadata and texture/font paths. PNGs and embedded TTFs are written under `textures/`
and `fonts/`. Each used VibeMO numeric font face has separate ASCII glyph metrics in `fontMetricsByAsset`; the
legacy `fontMetrics` field names the first one. A metrics file's texture is relative to that file when its
`textureBase` is `"metrics"`. Rotated packed sprites are decoded to standalone RGBA images.
Each `uipacks/<id>.json` is a `nnnotes.product/1` manifest for `ui.pack`. Its `files` map contains `ui.json`, and its
`objects` map lists each retained object's files as content-addressed assets. Texture images, embedded Font data,
TMP font records, clips, controllers and prefabs use the shared object conversion layer.

The index records hashes of every pack and its texture/font/metrics files, as well as dependency IDs. A resumed
key is skipped only when these files and its dependency records still validate. Missing or corrupt files are
re-exported. Provenance records hashes of the APK catalog and effective APK-set member metadata, the manifest
version and configured region; it contains no credentials or server addresses. A different input is rejected
before updating an existing library. The index is replaced atomically after each key.
`ui.json` has `format: "nnnotes.ui-pack/1"`, the catalog `key`, `roots`, `prefabs`, `controllers`, display `entries`,
`shaders`, and per-prefab animation `bindings`. Nodes preserve their serialized transforms, rectangles, active
flags, component type/script/fields and path IDs. References use object identity or a local hierarchy target.
Clips retain their raw curves; bindings name each curve's path and property in the Animator's hierarchy.

## Scope
UI products include the fonts referenced by their object graph. Product stages opt into this traversal with
`INCLUDE_FONTS`; language-specific story products select fonts through their own products.

This is inspectable serialized data, not an implementation of every game Presenter. The preview implements a
subset of layout, graphics, component binding and animation. Exported particle, shader/material, localization,
video, camera and custom component fields do not imply that the preview executes them. `runtime_verified` stays
false. The companion player documentation lists rendering limits and the optional viewer.
See [ui-pack schema](schemas.md) and [products](products.md) for the format contracts. The preview module provides
Canvas 2D inspection, animation sampling and editable copies of prefab state. It supports a defined subset of
runtime behavior; its API documents the rendering and animation limits.
13 changes: 12 additions & 1 deletion src/nnnotes/cli/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def matches(self, stage: str) -> bool:
StageSource("story.simpleui", "nnnotes.pipeline.story:SimpleUiStage", output=True, product=True),
StageSource("story.host", "nnnotes.pipeline.storyhost:HostStage", output=True, product=True),
StageSource("story", "nnnotes.pipeline.story:StoryStage", output=True, product=True),
StageSource("ui.pack", "nnnotes.pipeline.ui:UiStage", output=True, product=True),
StageSource("view.", "nnnotes.pipeline.views:stages", output=True),
StageSource("view.voices", "nnnotes.pipeline.voices:VoiceStage", output=True),
)
Expand Down Expand Up @@ -1064,7 +1065,17 @@ def _product_entries(self, name: str, placed: LayoutInput, store: bool) -> tuple
sha = self.depot.put(body) if store else contract.sha256(body)
manifest = {"id": contract.artifact_id(tid, "manifest"), "sha256": sha, "size": len(body)}
products.append((self.installed.stages[stage_name].manifest(subject), manifest, doc, table))
return layout.product_entries(products), {"collisions": [], "products": len(products)}
entries = layout.product_entries(products)
indexed = {}
for tid, doc in results:
stage_name, subject = contract.parse_task_id(tid)
if hasattr(self.installed.stages[stage_name], "layout_index"):
indexed.setdefault(stage_name, []).append((subject, doc))
for name, records in sorted(indexed.items()):
path, body = self.installed.stages[name].layout_index(records, self.depot)
sha = self.depot.put(body) if store else contract.sha256(body)
entries.append({"path": path, "sha256": sha, "size": len(body), "id": f"{name}:library#index"})
return entries, {"collisions": [], "products": len(products)}

def done(self, tasks: dict) -> None:
"""The tasks whose results the layouts place, {task id: key} (after a run: every task with a result)."""
Expand Down
42 changes: 18 additions & 24 deletions src/nnnotes/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from __future__ import annotations

import sys
from pathlib import Path

from ..core.jsonio import write_json
Expand Down Expand Up @@ -261,30 +260,26 @@ def cmd_audio(args, cfg):


def cmd_ui(args, cfg):
from ..content import ui
from ..pipeline.ui import UI

if args.limit is not None and args.limit < 1:
usage(args, "--limit must be positive")
index = ui.export_apk_library(
cfg,
args.out,
keys=args.key,
prefix=args.prefix,
limit=args.limit,
dependencies=not args.no_dependencies,
force=args.force,
)
report(
args,
{
"index": str(Path(args.out).resolve() / "index.json"),
"counts": index["counts"],
"embedded": len(index["embedded"]),
"controllers": len(index["controllers"]),
},
)
if index["counts"].get("failed"):
sys.exit(1)
cfg.require_path("paths", "apk")
use(cfg)
ws = assets.Workspace(args, cfg, layouts=["product"])
ws.run_index()
known = {
loc["primaryKey"] for loc in ws.index()["locations"] if loc["id"].startswith("apk:") and loc["kind"] == "asset"
}
keys = sorted(set(args.key) if args.key else {key for key in known if key.startswith(args.prefix)})
if missing := sorted(set(keys) - known):
usage(args, "UI key not in the APK catalog: " + ", ".join(missing))
if args.limit is not None:
keys = keys[: args.limit]
if not keys:
usage(args, "no UI keys selected")
ws.request({UI: keys}, [f"key:{key}" for key in keys])
assets.export(args, cfg, ws)


def product_args(c) -> None:
Expand Down Expand Up @@ -445,5 +440,4 @@ def register(sub) -> None:
c.add_argument("--key", action="append", help="exact APK catalog key (repeatable)")
c.add_argument("--prefix", default="EmbUI/", help="APK UI key prefix (default EmbUI/)")
c.add_argument("--limit", type=int, help="maximum number of keys")
c.add_argument("--no-dependencies", action="store_true", help="omit additional prefab roots and controllers")
c.add_argument("--force", action="store_true", help="re-export the selected keys")
product_args(c)
Loading
Loading