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
52 changes: 44 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ on:
tags: ["v*"]

permissions:
contents: write
contents: read

jobs:
release:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
Expand All @@ -24,6 +22,44 @@ jobs:
echo "tag $GITHUB_REF_NAME does not match the package version $version" >&2
exit 1
fi
- name: Build sdist and wheel
run: |
python -m pip install --upgrade pip build twine
python -m build
python -m twine check --strict dist/*
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
if-no-files-found: error

pypi:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/nnnotes/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
# trusted publishing: PyPI accepts this workflow's OIDC token, no API token is stored
- uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

github-release:
needs: pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release notes
uses: orhun/git-cliff-action@v4
with:
Expand All @@ -32,10 +68,10 @@ jobs:
env:
OUTPUT: RELEASE_NOTES.md
GITHUB_REPO: ${{ github.repository }}
- name: Build sdist and wheel
run: |
python -m pip install --upgrade pip build
python -m build
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: GitHub release
uses: softprops/action-gh-release@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Based on all data of the Taiwan server, version 1.0.1 (zh-Hant):
## Requirements

- Python 3.11+, 3.13 recommended (a `web` build reads and writes a lot of JSON, and the standard library encodes
JSON faster on 3.13); `pip install -e .` in the repository (not yet on PyPI)
JSON faster on 3.13); `pip install nnnotes` (for development `pip install -e .` in the repository)
- the game's `base.apk`: APK-local bundles, the CRI keycode, boot settings. `player`, `story`, `live` and `web` read
MonoBehaviours of its boot data with type trees that ship with nnnotes; supported now: game version 1.0.1
(Unity 6000.3.12f1). With an APK of another version whose classes do not match, these commands stop with an
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ nnnotes 是 BanG Dream! Our Notes 游戏文件的离线数据工具包:读取

## 需要准备

- Python 3.11+,推荐 3.13(`web` 构建读写大量 JSON,3.13 的标准库 JSON 编码更快),在仓库内 `pip install -e .`(尚未发布到 PyPI)
- Python 3.11+,推荐 3.13(`web` 构建读写大量 JSON,3.13 的标准库 JSON 编码更快),`pip install nnnotes`(开发时在仓库内 `pip install -e .`)
- 游戏安装包 `base.apk`:APK 内置资源包、CRI 解码密钥、启动设置。`player`、`story`、`live`、`web` 用 nnnotes 自带的类型树读取 APK 启动数据中的 MonoBehaviour,目前支持游戏版本 1.0.1(Unity 6000.3.12f1);其他版本的 APK 若类型不符,这些命令会报错并给出类名、游戏版本和 Unity 版本
- 解码后的 masterdata 目录(可用 `master download` + `master decode` 生成):`adv`、`story`、`spot`、`live` 和 `web` 的谱面需要;`web` 的 Live2D 模型只用它取角色名(可选)
- 外部工具:[vgmstream](https://vgmstream.org/)(CRI HCA 解码)、[FFmpeg](https://ffmpeg.org/)(转码,含剧情视频的 WebM 封装与 Opus 音频);`web` 另需 Node.js 20+ 与构建好的 ournotes-player
Expand Down
30 changes: 16 additions & 14 deletions src/nnnotes/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def _fonts_arg(c) -> None:

def _audio_args(c) -> None:
"""--format and --flac-level of a command that decodes cue sheets."""
c.add_argument("--format", default="flac", choices=AUDIO_CHOICES)
c.add_argument("--format", default="flac", choices=AUDIO_CHOICES, help="audio file format (default flac)")
c.add_argument("--flac-level", type=int, metavar="N",
help="FLAC compression level 0-12 of --format flac (default 8; every level decodes to the same "
"samples)")
Expand All @@ -573,17 +573,18 @@ def build_parser() -> argparse.ArgumentParser:
sub = p.add_subparsers(dest="cmd", required=True, metavar="<command>")

c = sub.add_parser("catalog", help="list addressable keys")
c.add_argument("--prefix", default="")
c.add_argument("--limit", type=int, default=200)
c.add_argument("--prefix", default="", help="only the keys that start with this prefix")
c.add_argument("--limit", type=int, default=200, help="print at most N keys (default 200; the total goes "
"to stderr)")
c.set_defaults(func=cmd_catalog)

c = sub.add_parser("browse", help="browse the configured regions' catalogs and bundles in a local web page")
c.add_argument("--port", type=int, default=8000)
c.add_argument("--host", default="127.0.0.1")
c.add_argument("--port", type=int, default=8000, help="port (default 8000)")
c.add_argument("--host", default="127.0.0.1", help="address to listen on (default 127.0.0.1)")
c.set_defaults(func=cmd_browse)

c = sub.add_parser("pull", help="fetch the bundle closure of keys into the cache")
c.add_argument("keys", nargs="+")
c.add_argument("keys", nargs="+", metavar="KEY", help="addressable keys (`catalog` lists them)")
c.set_defaults(func=cmd_pull, usage=c.error)

c = sub.add_parser("servers", help="the server list of the bootstrap API root: regions, their CDN and API roots")
Expand All @@ -608,12 +609,12 @@ def build_parser() -> argparse.ArgumentParser:
m.set_defaults(func=cmd_master_download)

c = sub.add_parser("adv", help="ADV episode -> JSON")
c.add_argument("adv_id", type=int)
c.add_argument("adv_id", type=int, help="MasterAdv id of the episode")
_out(c, "output .json file")
c.set_defaults(func=cmd_adv, usage=c.error)

c = sub.add_parser("story", help="ADV episode -> story dir (episode, models, audio, scene, UI, media, videos)")
c.add_argument("adv_id", type=int)
c.add_argument("adv_id", type=int, help="MasterAdv id of the episode")
_out(c, "output directory")
_audio_args(c)
c.add_argument("--no-audio", action="store_true", help="do not decode the cue sheets")
Expand All @@ -628,24 +629,24 @@ def build_parser() -> argparse.ArgumentParser:
c.set_defaults(func=cmd_live2d, usage=c.error)

c = sub.add_parser("spot", help="spot -> spot.json + Spine + room.glb + shaders")
c.add_argument("spot_id", type=int)
c.add_argument("spot_id", type=int, help="MasterSpot id")
_out(c, "output directory")
c.set_defaults(func=cmd_spot, usage=c.error)

c = sub.add_parser("room", help="background prefab -> glb")
c.add_argument("key")
c.add_argument("key", help="addressable key of the prefab")
_out(c, "output .glb file")
c.set_defaults(func=cmd_room, usage=c.error)

c = sub.add_parser("shader", help="dump shaders of a key's closure or of APK bundles")
g = c.add_mutually_exclusive_group(required=True)
g.add_argument("--key")
g.add_argument("--key", help="addressable key: the shaders of its bundle closure")
g.add_argument("--apk-bundle", nargs="+", help="substring(s) of APK bundle file names")
_out(c, "output directory")
c.set_defaults(func=cmd_shader, usage=c.error)

c = sub.add_parser("audio", help="decode a CRI cue sheet")
c.add_argument("cue_sheet")
c.add_argument("cue_sheet", help="cue sheet name: the key Cri/Sound/<cue_sheet> of the catalog")
_out(c, "output directory")
_audio_args(c)
c.set_defaults(func=cmd_audio, usage=c.error)
Expand All @@ -659,8 +660,9 @@ def build_parser() -> argparse.ArgumentParser:
c.set_defaults(func=cmd_player)

c = sub.add_parser("live", help="live (music + difficulty) -> self-contained live dir")
c.add_argument("music_id", type=int)
c.add_argument("--difficulty", default="expert", choices=DIFFICULTY_CHOICES)
c.add_argument("music_id", type=int, help="MasterLiveMusic id")
c.add_argument("--difficulty", default="expert", choices=DIFFICULTY_CHOICES,
help="chart difficulty (default expert)")
_audio_args(c)
_fonts_arg(c)
_band_args(c)
Expand Down
2 changes: 1 addition & 1 deletion src/nnnotes/cli_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ def bind(fn):
c = sub.add_parser("catalogs", help="catalog versions of the store")
csub = c.add_subparsers(dest="catalogs_cmd", required=True, metavar="<catalogs command>")
m = csub.add_parser("list", help="the imported catalog versions")
m.add_argument("--json", action="store_true")
m.add_argument("--json", action="store_true", help="the versions as JSON")
_store_arg(m)
m.set_defaults(func=bind(cmd_catalogs_list), usage=m.error)
m = csub.add_parser("import", help="import a catalog file (with the APK's catalog when [paths] apk is set)")
Expand Down
8 changes: 4 additions & 4 deletions src/nnnotes/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ def base(p, text=True):
p.add_argument("--source", help="a source (Talk, CharacterVoice, ..., HomeSpot, Title, Sound, Story)")
p.add_argument("--episode", metavar="ADV", help="the Story voices of one episode: a MasterAdv id or an "
"episode asset (without --from: read from the catalog)")
p.add_argument("--status", choices=STATUSES)
p.add_argument("--status", choices=STATUSES, help="only the rows with this status")
p.add_argument("--limit", type=int, default=0, help="at most N rows (0: all)")
p.add_argument("--json", action="store_true", help="the rows as JSON")

Expand All @@ -1345,17 +1345,17 @@ def done(p, fn):
p.add_argument("--language", choices=LANGUAGES, help="the language of the text column ([catalog] language)")
done(p, cmd_list)
p = vs.add_parser("search", help="the voices whose text (or cue name) contains TEXT")
p.add_argument("text")
p.add_argument("text", help="text to look for")
base(p)
p.add_argument("--language", choices=LANGUAGES, help="search this language only (default: all five)")
done(p, cmd_search)
p = vs.add_parser("summary", help="rows per status, category and character; gaps both ways")
base(p, text=False)
p.add_argument("--json", action="store_true")
p.add_argument("--json", action="store_true", help="the summary as JSON")
done(p, cmd_summary)
p = vs.add_parser("get", help="the files of one voice: a row id (MasterTalk:561, MasterAdv:<id>:<command>) or a "
"sound id")
p.add_argument("id")
p.add_argument("id", help="row id or sound id")
p.add_argument("-o", "--out", required=True, help="output directory")
base(p, text=False)
p.add_argument("--format", choices=("flac", "ogg", "wav"), default="flac",
Expand Down
Loading