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
3 changes: 3 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ a live (music + difficulty) -> the live product and the products it plays with (
| `--band` `BAND` | band of the LightWeight background and start timeline (default: band of the music's first vocal character) |
| `--leader-card` `LEADER_CARD` | deck centre MasterMemberCard id (its character's band) |
| `--live-option` `OPTION[=VALUES]` | also export the files of a Live option's variants (repeatable): MirrorChart, MeasureLineDisplay, NoteDesignId, NoteEffectId, LiveQuality, NoteSePatternId (every value of the master data, or =v,v...), defaults (the option defaults and ranges only) or all |
| `--live-languages` `CODES` | comma-separated UI languages, or all (default: catalog language) |
| `--fonts` `FONTS` | glyph source: open font files (default) or game TMP assets (one of `open`, `game`) |
| `--font` `LANG=PATH` | font file for a language (repeatable; [paths] fonts.<lang>) |
| `-o`, `--out` `OUT` | output directory (required) |
| `--stored-catalog` `LABEL\|SHA` | a catalog version of the depot (`catalog versions`; default: the current catalog) |
| `--workers` `N` | worker processes (default: the usable CPUs) |
Expand Down
10 changes: 10 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ status 2 and a line naming the class, the game version and the Unity version (as
nnnotes export live MUSIC_ID -o OUT [--difficulty easy|normal|hard|expert] [--format aac|flac|mp3|opus|vorbis]
[--flac-level N] [--band BAND | --leader-card CARD_ID]
[--live-option OPTION[=VALUES] ...]
[--live-languages LANG,...] [--fonts open|game] [--font LANG=PATH]
```

One chart (music + difficulty, default `expert`) and the products it plays with, in the product layout
Expand All @@ -396,6 +397,7 @@ OUT/noteskins/<skin>.json a note skin
OUT/noteeffects/<set>.json a note effect set
OUT/sounds/<sets>.json the note and live sounds with their CRI routing and volumes
OUT/music/<musicId>.json the BGM in a web format
OUT/liveui/<musicId>_<difficulty>_<lang>.json localized start-canvas text, glyph pages and image bindings
OUT/assets/<sha256>.<ext> every file the manifests name, once
```

Expand All @@ -409,6 +411,14 @@ of the music's first vocal character; `live.json` `band` records the choice, and
band only when one of the two flags is given. A band whose start timeline or background the catalog does not hold is
a usage error.

`--live-languages` selects UI languages separated by commas (`all` selects all client languages); the default is
`[catalog] language`. `--fonts open` (default) generates glyph pages from `[paths.fonts]` or repeatable
`--font LANG=PATH` flags. Supply fonts for the requested language and its fallback languages. `--fonts game` uses
the client's TMP assets. Both modes require the `fonts` extra and use the client's font metrics and layout
settings. Font bytes participate in the task key. The UI's canvas and difficulty icons are in `live.scene`, and
its jacket and text shaders are in the UI product's object table. `live.json` names UI products by language in
`parts.ui`.

The products hold what a live reads with the game's default options (option preset 1 of a fresh profile), and always
the mirrored notes, the bar line view and the option tables. `--live-option` (repeatable) adds the products of other
values of the options that select files, so that a player can offer them; the options are named as in the game (the
Expand Down
4 changes: 4 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Episode font data uses the `format` field `nnnotes.story-fonts/1`, described in
[story-fonts.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/story-fonts.schema.json)
and [story products](products.md#stories).

Live start UI data uses the `format` field `nnnotes.live-ui/1`, described in
[live-ui.schema.json](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/schemas/live-ui.schema.json)
and [live start UI products](products.md#live-start-ui).

Stages carry their own version. A change of what a stage writes for the same inputs is a new stage version, which
changes the key of every task of that stage (golden tests fail when a stage's output changes while its version
stays).
Expand Down
9 changes: 7 additions & 2 deletions docs/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,23 @@ Details: [export episode](commands.md#export-episode), [export story](commands.m
## A live

```sh
nnnotes export live 100001 --difficulty expert -o out/lives/100001_expert
nnnotes export live 100001 --difficulty expert --fonts game -o out/lives/100001_expert
```

One chart and the products it plays with, in the layout ournotes-player reads: the chart as shipped and its
converted runtime notes, the 3D scene, the note highway with its skins and effect sets, the sound effects with their
routing, and the BGM in a web format.
routing, the BGM in a web format, and localized start-canvas text with glyph pages.

- `--band BAND` or `--leader-card CARD_ID` picks the band of the background and start timeline (default: the band of
the music's first vocal character).
- `--live-option NAME[=VALUES]` (repeatable) adds the products of other values of the live options, such as
`NoteDesignId` or `NoteSePatternId`; `--live-option all` adds every option with every value.

- `--live-languages ja,en` selects UI languages (default: catalog language; `all` selects all client languages).
- `--fonts game` uses the client's TMP fonts. The default, `--fonts open`, reads font files from `[paths.fonts]`
or repeatable `--font LANG=PATH` flags. Supply the languages used by the fallback chain. Both modes need the
`fonts` extra.

Details: [export live](commands.md#export-live).

## A Live2D model
Expand Down
25 changes: 25 additions & 0 deletions docs/products.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,31 @@ The object table holds the jacket Sprite and its texture; the chart's `TextAsset
layout makes the live of each chart whose key and jacket the selection holds (with the master data), without a band.
`export live` asks for one live and its parts ([export live](commands.md#export-live)).

### Live start UI

`live.ui` has subject `<musicId>/<difficulty>/<language>` and manifest
`liveui/<musicId>_<difficulty>_<language>.json`. It reads the live scene, the jacket, the client's font assets,
LocalizeManager and TMP Settings, plus the chart's master rows. `ui.json` uses format `nnnotes.live-ui/1`.

- `scene` identifies the manifest, prefab and canvas path in `live.scene`.
- `strings` contains the title, singer, credits, level and a fresh profile's high score (`0`). The title uses the
Japanese column; singer and credit labels use the requested language. Empty localized strings stay empty.
- `texts` maps TMP component ids to their path, displayed `text`, `setByInitialize`, localized layout `style`,
original `localized` font selection and generated `font` selection.
- `images` maps Image component ids to the selected jacket and difficulty Sprite ids. `difficultyIcons` lists
the canvas's icons, and `jacket` identifies the jacket. Canvas components and difficulty sprites belong to
`live.scene`; the jacket and its texture belong to this product.
- `fonts`, `materials`, `textures`, `materialKeywords`, `coverage`, `lineBreaking` and `shaders` describe the
generated text resources, with the same fields as the story font product.

The `fonts` parameter is `open` (default) or `game`. Both modes generate glyph pages and use the client's font
metrics, layout settings and fallback chains. Open fonts use explicit `font:<language>` byte inputs; their
contents participate in the task key. The exported object table holds the jacket and text shaders. The canvas
hierarchy is shared through `live.scene`.

`live.json` includes `parts.ui`, a language-to-manifest map. `export live` requests the languages selected by
`--live-languages`, defaulting to `[catalog] language`.

## Stories

A story episode plays with products that several episodes share, made by `nnnotes.pipeline.story`:
Expand Down
1 change: 1 addition & 0 deletions docs/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ schemas.load("plan") # the schema as a dict
| `coverage` | `nnnotes.coverage/1` | the coverage of a run per class and per reason code |
| `failures` | `nnnotes.failures/1` | the failed tasks and items of a run |
| `layout` | `nnnotes.layout/1` | the files a layout owns in its output directory |
| `live-ui` | `nnnotes.live-ui/1` (`format`) | live start strings, text styles, glyph pages and image bindings |
| `music-data` | `nnnotes.music-data/2` | every song and chart of one master data version ([music data](music-data.md)) |
| `objects` | `nnnotes.objects/1` | the objects a product reaches: their class, item status and files ([products](products.md)) |
| `plan` | `nnnotes.plan/1` | what a run would do and why (`depot plan`) |
Expand Down
1 change: 1 addition & 0 deletions docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ codec and channel of a USM) and `catalog.index` (version 2: gzip input and Japan
| `live.noteeffect` | a note effect set (`effect001`, `effect001Light`) | `objects`: the object table of the effect settings | `noteeffect.json` and the object table |
| `live.scene` | `default` | `objects`: the object table of the scene, the bands' start timelines and backgrounds, the lane skins, the UI assets and the built-in graphics settings ([products](products.md#live-scene)); `master`: the master record of its settings, lane skins and bands | `scene.json` and the object table |
| `live` | a live: `<musicId>/<difficulty>[/<band>]` | `objects`: the object table of the chart and the jacket ([products](products.md#lives)); `master`: the live's master record and the manifest paths of its parts | `live.json`, `chart.json`, `notes.json`, `notes.mirror.json` and the object table |
| `live.ui` | `<musicId>/<difficulty>/<language>` | `objects`: scene, fonts, jacket and localization settings; `master`; `jacket.objects`; `font-bundles`; `apk:AndroidManifest.xml`; explicit `font:<language>` inputs in open mode; parameter `fonts` (`open` or `game`) | `ui.json`, glyph pages, jacket and text shaders ([products](products.md#live-start-ui)) |
| `story.sound` | a cue sheet name | `objects`; `acb:<key>` and `sheets`: the cue sheet and its `cri.audio` files ([products](products.md#stories)); parameter `audio` (`aac`) | `cues.json` (each cue's nonempty CRI `categories` included) and the cues' sounds in the web format |
| `story.crilips` | `default` | `objects` (empty); `apk:lib/arm64-v8a/libcri_lips_unity.so` | `crilips.json` and `crilips.bin` |
| `story.scene` | `default` | `objects`: the object table of the ADV scene, its prefabs and settings and the built-in graphics settings ([products](products.md#stories)) | `scene.json` and the object table |
Expand Down
3 changes: 2 additions & 1 deletion src/nnnotes/cli/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def matches(self, stage: str) -> bool:
StageSource("live.noteeffect", "nnnotes.pipeline.live:NoteEffectStage", output=True, product=True),
StageSource("live.scene", "nnnotes.pipeline.live:SceneStage", output=True, product=True),
StageSource("live", "nnnotes.pipeline.live:LiveStage", output=True, product=True),
StageSource("live.ui", "nnnotes.pipeline.live:UiStage", output=True, product=True),
StageSource("story.sound", "nnnotes.pipeline.story:SoundStage", output=True, product=True),
StageSource("story.crilips", "nnnotes.pipeline.story:CriLipsStage", output=True, product=True),
StageSource("story.scene", "nnnotes.pipeline.story:SceneStage", output=True, product=True),
Expand Down Expand Up @@ -771,7 +772,7 @@ def facts(self, pipeline: list[str], fetch: bool, background: bool = False) -> d
"raw": Inputs({}),
"product_inputs": self.product_inputs,
}
if "story.fonts" in products:
if {"story.fonts", "live.ui"}.intersection(products):
from ..source.languages import LANGUAGES

font_paths = {code: self.cfg.path("paths.fonts", code) for code in (*LANGUAGES, "emoji")}
Expand Down
32 changes: 31 additions & 1 deletion src/nnnotes/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def cmd_live(args, cfg):
from ..content import live, liveaudio, livenotes, liveoptions
from ..pipeline import live as lp
from ..pipeline.stages import Env
from ..source.languages import check
from ..source.master import table

cfg.require_path("paths", "apk") # the built-in graphics and sound settings and the CRI ACF are in the APK
Expand All @@ -140,18 +141,28 @@ def cmd_live(args, cfg):
ws = assets.Workspace(args, cfg, layouts=["product"])
env = Env(ws.depot, {"master": md})
stage, subject = lp.LiveStage(), live.live_subject(args.music_id, args.difficulty, band)
wanted_languages = getattr(args, "live_languages", None) or [check(cfg.get("catalog", "language"))]
try: # no chart of the difficulty, a music without a vocal character, a master table missing
b = stage.live_record(subject, env)["band"]["band"]
products = {lp.LIVE: [subject], **stage.parts(subject, env)}
products = {lp.LIVE: [subject], **stage.parts(subject, env, wanted_languages)}
except (KeyError, ValueError) as e:
usage(args, e.args[0] if e.args else type(e).__name__)
ws.request(products, [])
ws.params(ws.pipeline()) # the flags the stages take, checked before the index
ws.run_index()
env.facts["index"] = ws.index()
held = {loc["primaryKey"] for loc in ws.index()["locations"]}
missing = [k.format(band=b) for k in live.BAND_KEYS if k.format(band=b) not in held]
if missing:
usage(args, f"band {b}: {', '.join(missing)} not in the catalog")
if getattr(args, "fonts", "open") == "open":
supplied = dict(getattr(args, "font", None) or [])
for code in wanted_languages:
path = Path(supplied[code]) if code in supplied else cfg.path("paths.fonts", code)
if path is None or not path.is_file():
raise ConfigError(
f"the live text of {code} needs a font file: --font {code}=PATH or [paths.fonts] {code}"
)
skins = {r["_id"]: r["_assetName"] for r in table(md, "MasterLiveNoteSkin")}
effects = {r["_id"]: r["_assetName"] for r in table(md, "MasterLiveNoteEffectSkin")}
products[lp.NOTESKIN] += [skins[i] for i in options.designs if skins[i] not in products[lp.NOTESKIN]]
Expand Down Expand Up @@ -376,6 +387,25 @@ def register(sub) -> None:
flac_level_arg(c, "of the decoded sounds")
band_args(c)
live_option_arg(c)
c.add_argument(
"--live-languages",
type=parse_languages,
metavar="CODES",
help="comma-separated UI languages, or all (default: catalog language)",
)
c.add_argument(
"--fonts",
choices=("open", "game"),
default="open",
help="glyph source: open font files (default) or game TMP assets",
)
c.add_argument(
"--font",
type=parse_font,
action="append",
metavar="LANG=PATH",
help="font file for a language (repeatable; [paths] fonts.<lang>)",
)
out_arg(c, "output directory")
product_args(c)

Expand Down
37 changes: 28 additions & 9 deletions src/nnnotes/content/liveui.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@

import json
from pathlib import Path
from typing import TYPE_CHECKING

from ..core.jsonio import write_json
from ..source import languages
from ..source.catalog import Catalog
from ..unity import fontsettings, textstyle, tmpfont
from ..unity.export import Exporter, TexelPacker, _safe
from ..unity.player import PlayerData
from ..unity import fontsettings, textstyle
from ..unity.textstyle import LANGUAGE_LINE_SPACING
from .score import master_table

if TYPE_CHECKING:
from ..source.catalog import Catalog
from ..unity.export import Exporter, TexelPacker
from ..unity.player import PlayerData

SCENE_KEY = "EmbScene/Live"
CANVAS = "Live/UILiveStartCanvas"
Expand Down Expand Up @@ -66,8 +68,19 @@ def _comp(node: dict, cls: str) -> dict | None:
def resolve_strings(master: Path, music_id: int, difficulty: str, column: str) -> tuple[dict, dict]:
"""The strings UILiveStartCanvas.Initialize sets (IReadOnlyLiveMusicScore slots 2..8), for a fresh profile, with
the text table column `column` of the current LanguageMode. -> (strings, sources)."""
music = next(r for r in master_table(master, "MasterLiveMusic") if r["_id"] == music_id)
text = {r["_id"]: r for r in master_table(master, "MasterText")}
from .score import master_table

tables = {
name: master_table(master, name)
for name in ("MasterLiveMusic", "MasterText", "MasterBand", "MasterLiveMusicScore")
}
return record_strings(tables, music_id, difficulty, column)


def record_strings(tables: dict, music_id: int, difficulty: str, column: str) -> tuple[dict, dict]:
"""The live start strings and their sources from explicit master-table rows."""
music = next(r for r in tables["MasterLiveMusic"] if r["_id"] == music_id)
text = {r["_id"]: r for r in tables["MasterText"]}

def localized(tid: str) -> str:
# MasterLoader.GetLocalizedText in the current LanguageMode: the column as it is, "" included (no
Expand All @@ -93,10 +106,10 @@ def credit(label_id: str, word_id: str) -> str:
# GetBandNameTextId: _bandNameTextID, else MasterBand(_bandIDs[0])._nameTextID
band_tid = music["_bandNameTextID"]
if not band_tid:
band = next(r for r in master_table(master, "MasterBand") if r["_id"] == music["_bandIDs"][0])
band = next(r for r in tables["MasterBand"] if r["_id"] == music["_bandIDs"][0])
band_tid = band["_nameTextID"]
score_id = music[f"_{difficulty}ID"]
score = next(r for r in master_table(master, "MasterLiveMusicScore") if r["_id"] == score_id)
score = next(r for r in tables["MasterLiveMusicScore"] if r["_id"] == score_id)
strings = {
"musicTitle": title,
"singerName": localized(band_tid),
Expand Down Expand Up @@ -145,6 +158,8 @@ def credit(label_id: str, word_id: str) -> str:
def _game_fonts(ex: Exporter, fontset, texts: dict, chars_by_font: dict, packer: TexelPacker):
"""The game's TMP fonts of the texts, reduced to the characters shown (tmpfont), their atlas blocks added to
`packer`. -> (glyph coverage, materials, fonts)."""
from ..unity import tmpfont

coverage: dict[str, dict] = {}
needed: dict[str, set] = {}
runtime_units: dict[str, set] = {}
Expand Down Expand Up @@ -191,6 +206,10 @@ def extract(
<out_dir>/livescene/shaders/shaders.json (livescene.extract) for the shader check. `language`: the client
language (a languages.LANGUAGES code); `fonts`: "open" (text records, font names only) or "game" (the game's
TMP fonts reduced to the characters shown; needs the `fonts` extra)."""
from ..unity import tmpfont
from ..unity.export import Exporter, TexelPacker, _safe
from .score import master_table

out_dir = Path(out_dir)
base = out_dir / "liveui"
base.mkdir(parents=True, exist_ok=True)
Expand Down
Loading
Loading