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: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
run: |
python -m ruff check .
python -m ruff format --check .
lint-imports
- name: Command line
run: python -m nnnotes --help
- name: Test
Expand All @@ -62,4 +63,4 @@ jobs:
- name: Golden records
env:
GOLDENS_STRICT: "1"
run: python -m pytest tests/test_goldens.py tests/test_objexport.py
run: python -m pytest tests/pipeline/test_goldens.py tests/pipeline/test_objexport.py
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ overriding earlier ones:
`--vgmstream`, `--node` go before the command name; `--player` is an option of `web`

`nnnotes config init` writes the config template (every setting empty; it ships with the package, in the repository
[`src/nnnotes/nnnotes.example.toml`](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/nnnotes.example.toml)),
[`src/nnnotes/source/nnnotes.example.toml`](https://github.com/MetaSekaiLab/nnnotes/blob/main/src/nnnotes/source/nnnotes.example.toml)),
asking for each value in a terminal; `config set` / `config unset` change one value and keep the file's comments.
The settings are: the bundle
key and nonce seed, the master data key and IV, the region in use (`[catalog] region`) and the catalog language
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ nnnotes config check # 每项设置的来源和格式是否有效,
2. 环境变量:`NNNOTES_<节>_<键>`(如 `NNNOTES_BUNDLE_KEY`、`NNNOTES_SERVERS_TW_CDN`)
3. 命令行参数:`--region`、`--language`、`--catalog`、`--cache`、`--master`、`--apk`、`--ffmpeg`、`--vgmstream`、`--node` 写在命令名之前;`--player` 是 `web` 的参数

`nnnotes config init` 写出配置模板(每项设置为空,模板随包安装,仓库中为 [`src/nnnotes/nnnotes.example.toml`](src/nnnotes/nnnotes.example.toml)),在终端中会逐项询问;`config set` / `config unset` 修改单项并保留文件中的注释。需要的设置包括:资源包解密密钥与 nonce 种子、masterdata 的密钥与 IV、使用的区服 `[catalog] region` 与 catalog 语言 `[catalog] language`、每个区服的 CDN 地址与 API 根地址(`[servers.<区服>]` 的 `cdn`、`api`)、客户端版本 `[client] version`(未设置时读取 APK 的 versionName)、可选的引导 API 根地址 `[bootstrap] api`(`servers` 使用),以及缓存目录、APK、masterdata 目录、ournotes-player 和 vgmstream / FFmpeg / Node.js 的路径(三个工具未设置时在 `PATH` 中查找)。这些值都来自使用者自己的游戏客户端。
`nnnotes config init` 写出配置模板(每项设置为空,模板随包安装,仓库中为 [`src/nnnotes/source/nnnotes.example.toml`](src/nnnotes/source/nnnotes.example.toml)),在终端中会逐项询问;`config set` / `config unset` 修改单项并保留文件中的注释。需要的设置包括:资源包解密密钥与 nonce 种子、masterdata 的密钥与 IV、使用的区服 `[catalog] region` 与 catalog 语言 `[catalog] language`、每个区服的 CDN 地址与 API 根地址(`[servers.<区服>]` 的 `cdn`、`api`)、客户端版本 `[client] version`(未设置时读取 APK 的 versionName)、可选的引导 API 根地址 `[bootstrap] api`(`servers` 使用),以及缓存目录、APK、masterdata 目录、ournotes-player 和 vgmstream / FFmpeg / Node.js 的路径(三个工具未设置时在 `PATH` 中查找)。这些值都来自使用者自己的游戏客户端。

缺少或格式错误的设置会让命令以退出码 2 结束,并用一行说明对应的配置键、环境变量和命令行参数,不会输出任何设置值。`nnnotes.toml` 已在 `.gitignore` 中,请勿提交。

Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ root), and `stories.json` counts their files in `size.models`. The format is des
window of an Overlay story is handled the same way. Each generated asset takes the
point size, padding, style settings and render mode of the game font asset the texts use in that language, so the
game's text materials apply unchanged; those values, the game's fallback chains, the characters the game's assets
have and the text materials come from the table of them shipped with nnnotes (`src/nnnotes/fontsettings.json`,
have and the text materials come from the table of them shipped with nnnotes (`src/nnnotes/unity/fontsettings.json`,
`fontsettings.FORMAT`), so an open-font export reads no game font asset, material or font bundle;
its face info and glyph metrics come from the font file (FreeType, no
hinting), its distance field from the generator `--fonts game` uses for runtime glyphs (supersampled render modes
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Lowest to highest:
3. **Command-line flags**, for the settings that have one (table below).

An empty value (`""`, `[]`, an empty environment variable, an empty flag) counts as unset, so a lower source still
applies. The template [`nnnotes.example.toml`](../src/nnnotes/nnnotes.example.toml) ships with the package and lists
applies. The template [`nnnotes.example.toml`](../src/nnnotes/source/nnnotes.example.toml) ships with the package and lists
every setting with an empty value; `nnnotes config init` writes it (below). `nnnotes.toml` and `*.local.toml` are in
`.gitignore`; do not commit a filled-in copy.

Expand Down
8 changes: 4 additions & 4 deletions docs/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ gzip input and Japanese remote asset placeholders).
| `view.<name>` | a catalog (`main`) | `master:<Table>` 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.<name>:<subject>#view` (facts: rows, entries, gaps, unreferenced) |
| `view.voices` | a catalog (`main`) | `master:<Table>` 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:<sha256>` (the ACB of each decoded one), `episodes` (per story episode and part, the `unity.export` artifact holding the object its key names), `adv:<sha256>` (those contents); context `audio` (the keys of those `cri.audio` results); runs after `unity.export` and `cri.audio` | `view.voices:<subject>#view` (facts: rows, entries, gaps, unreferenced streams) |

`view.voices` is a stage of its own (`nnnotes.voices:VoiceStage`), not one of the master-only views of
`nnnotes.views:stages`: besides master tables and the address table it reads the story episodes of the
`view.voices` is a stage of its own (`nnnotes.pipeline.voices:VoiceStage`), not one of the master-only views of
`nnnotes.pipeline.views:stages`: besides master tables and the address table it reads the story episodes of the
`unity.export` results, `cri.audio` results and ACB tables ([views.md](views.md#voices)).

## Converters (`unity.export`)
Expand Down Expand Up @@ -128,14 +128,14 @@ else found on PATH. The `boot` input is the APK's `assets/bin/Data/data.unity3d`

## Atoms

`nnnotes.atoms.ATOMS[name]` is the reference implementation of an atom: its id, its function and its cost estimate.
`nnnotes.pipeline.atoms.ATOMS[name]` is the reference implementation of an atom: its id, its function and its cost estimate.
The id names the libraries whose behaviour the output depends on, with their installed versions, and the revision of
the nnnotes code (`pillow-12.1.1/1`); `nnnotes/<revision>` when no library's behaviour is involved. Task keys include
the ids of the atoms a task uses, so upgrading a library or changing an atom re-runs exactly the tasks that use it.

| Atom | Reference implementation | Id | Does |
|---|---|---|---|
| `reader` | `nnnotes.atoms.reader.open_bundle` (UnityPy) | `unitypy-<v>/1` | one bundle loaded alone from its bytes: serialized files, objects, raw bytes, typetrees, container, externals, and the inputs of `texture.decode` and `mesh.arrays` (resource files are read from inside the bundle only) |
| `reader` | `nnnotes.pipeline.atoms.reader.open_bundle` (UnityPy) | `unitypy-<v>/1` | one bundle loaded alone from its bytes: serialized files, objects, raw bytes, typetrees, container, externals, and the inputs of `texture.decode` and `mesh.arrays` (resource files are read from inside the bundle only) |
| `texture.decode` | UnityPy `Texture2DConverter.parse_image_data` | `unitypy-<v>+texture2ddecoder-<v>+astc-encoder-py-<v>+pillow-<v>/1` | the first mip level, top row first, as UnityPy's `Texture2D.image` |
| `png.encode` | Pillow | `pillow-<v>/1` | PNG bytes; level 6 (the default) gives Pillow's default bytes, the ones the other commands write |
| `astc.container` | nnnotes | `nnnotes/1` | the standard `.astc` file (16-byte header, first mip level's blocks) |
Expand Down
10 changes: 5 additions & 5 deletions docs/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

A view answers, for every row of a master table, which objects of the catalog the game shows for it: the full
illustration of a member card and its sub-sprites, the icon of a stamp, the jacket of a song, the banner of a story
episode, the icon of a reward. The rules are data (`src/nnnotes/viewrules.json`, package data) read by
`nnnotes.views`; a view document lists object ids, so it depends on the master data and the catalog, never on how
episode, the icon of a reward. The rules are data (`src/nnnotes/pipeline/viewrules.json`, package data) read by
`nnnotes.pipeline.views`; a view document lists object ids, so it depends on the master data and the catalog, never on how
the objects are encoded.

## Rules
Expand Down Expand Up @@ -108,7 +108,7 @@ resolver's name).

## Stages

`nnnotes.views:stages` gives one stage per view, `view.<name>` (docs/stages.md), run by `nnnotes export --views`
`nnnotes.pipeline.views:stages` gives one stage per view, `view.<name>` (docs/stages.md), run by `nnnotes export --views`
and planned by `nnnotes plan`:

- Subjects: the catalog subjects of `link.addresses` (the export command has one, `main`), when master data is set
Expand Down Expand Up @@ -474,7 +474,7 @@ icon: `not-applicable`):

## Voices

`view.voices` (`nnnotes.voices`) indexes the character voices the master data names and those of the story
`view.voices` (`nnnotes.pipeline.voices`) indexes the character voices the master data names and those of the story
episodes: per voice its source row, characters, category, text in the five languages and conditions, its cue sheet
and cue, and the decoded streams of the export that play it. It is a stage of its own rather than a view of the
rules above: besides master tables and the address table it reads the results of `unity.export` (the episodes) and
Expand Down Expand Up @@ -579,7 +579,7 @@ A voice of a home or after-live episode is often a cue that a row of another sou
### Cues and streams

The sound's `_soundCueSheetID` names the cue sheet (MasterSoundCueSheet `_cueSheetName`, catalog key
`Cri/Sound/<sheet>`) and `_cueName` the cue. The cue's streams come from the sheet's ACB tables (`nnnotes.acb`): a
`Cri/Sound/<sheet>`) and `_cueName` the cue. The cue's streams come from the sheet's ACB tables (`nnnotes.formats.acb`): a
cue references a waveform, a synth, a sequence or a block sequence; synths reference waveforms, synths and
sequences; the tracks of sequences and blocks note on synths and sequences. A waveform of the memory AWB is the
stream at its position in the AWB, the numbering of the sheet's `streams.json`. A cue may play several streams (a
Expand Down
22 changes: 20 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ classifiers = [
dynamic = ["version"]

[project.optional-dependencies]
test = ["pytest>=8", "ruff>=0.15", "jsonschema>=4.18"]
test = ["pytest>=8", "ruff>=0.15", "jsonschema>=4.18", "import-linter>=2.1"]
fonts = ["fonttools>=4.50", "freetype-py>=2.5", "scipy>=1.13"]
# the deck model of music-data: one nnnotes version carries one ournotes-deck release (.github/workflows/deck.yml moves
# the pin when ournotes-deck publishes a newer one)
Expand Down Expand Up @@ -65,4 +65,22 @@ select = ["E", "W", "F", "I", "UP", "B"]
ignore = ["B023", "B905"]

[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["nnnotes.liveoptions.LiveOptions"]
extend-immutable-calls = ["nnnotes.content.liveoptions.LiveOptions"]

[tool.importlinter]
root_package = "nnnotes"

[[tool.importlinter.contracts]]
name = "Layers: each subpackage imports only the layers below it"
type = "layers"
layers = [
"cli",
"site | musicdata",
"pipeline",
"content",
"unity",
"formats",
"source",
"core",
]
containers = ["nnnotes"]
Loading
Loading