nnnotes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nnnotes/__init__.py +7 -0
- nnnotes/__main__.py +4 -0
- nnnotes/acb.py +190 -0
- nnnotes/addressables.py +413 -0
- nnnotes/adv.py +243 -0
- nnnotes/advcommand.py +29 -0
- nnnotes/advmedia.py +178 -0
- nnnotes/advscene.py +113 -0
- nnnotes/advui.py +1154 -0
- nnnotes/advvideo.py +684 -0
- nnnotes/atoms/__init__.py +151 -0
- nnnotes/atoms/astc.py +52 -0
- nnnotes/atoms/gltf.py +290 -0
- nnnotes/atoms/mesh.py +289 -0
- nnnotes/atoms/png.py +28 -0
- nnnotes/atoms/reader.py +269 -0
- nnnotes/atoms/sniff.py +198 -0
- nnnotes/atoms/sprite.py +236 -0
- nnnotes/atoms/texture.py +80 -0
- nnnotes/cache.py +289 -0
- nnnotes/catalog.py +373 -0
- nnnotes/catalogdb.py +457 -0
- nnnotes/census.py +276 -0
- nnnotes/cli.py +744 -0
- nnnotes/cli_assets.py +1539 -0
- nnnotes/config.py +238 -0
- nnnotes/contract.py +497 -0
- nnnotes/cri.py +371 -0
- nnnotes/crikey.py +71 -0
- nnnotes/crilips.py +265 -0
- nnnotes/cristages.py +558 -0
- nnnotes/deckdata.py +462 -0
- nnnotes/export.py +1462 -0
- nnnotes/gameapi.py +318 -0
- nnnotes/jsonio.py +98 -0
- nnnotes/languages.py +44 -0
- nnnotes/layout.py +596 -0
- nnnotes/link.py +418 -0
- nnnotes/live.py +115 -0
- nnnotes/live2d.py +235 -0
- nnnotes/liveaudio.py +331 -0
- nnnotes/livenotes.py +291 -0
- nnnotes/liveoptions.py +250 -0
- nnnotes/livescene.py +450 -0
- nnnotes/liveui.py +328 -0
- nnnotes/master.py +251 -0
- nnnotes/modelstages.py +424 -0
- nnnotes/motion.py +131 -0
- nnnotes/objexport.py +1564 -0
- nnnotes/orchestrate.py +630 -0
- nnnotes/plan.py +218 -0
- nnnotes/player.py +392 -0
- nnnotes/room.py +391 -0
- nnnotes/score.py +1139 -0
- nnnotes/shader.py +217 -0
- nnnotes/spot.py +201 -0
- nnnotes/stages.py +285 -0
- nnnotes/store.py +518 -0
- nnnotes/story.py +142 -0
- nnnotes/storyfonts.py +1258 -0
- nnnotes/storyhost.py +1190 -0
- nnnotes/storysite.py +662 -0
- nnnotes/storyvoices.py +102 -0
- nnnotes/textstyle.py +355 -0
- nnnotes/tmpfont.py +636 -0
- nnnotes/typetrees/6000.3.12f1.json +19 -0
- nnnotes/unity.py +368 -0
- nnnotes/viewrules.json +441 -0
- nnnotes/views.py +966 -0
- nnnotes/voices.py +1363 -0
- nnnotes/web.py +1624 -0
- nnnotes/webaudio.py +14 -0
- nnnotes/webmodel.py +397 -0
- nnnotes-0.1.0.dist-info/METADATA +200 -0
- nnnotes-0.1.0.dist-info/RECORD +79 -0
- nnnotes-0.1.0.dist-info/WHEEL +5 -0
- nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
- nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
- nnnotes-0.1.0.dist-info/top_level.txt +1 -0
nnnotes/cli.py
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"""nnnotes command line.
|
|
2
|
+
|
|
3
|
+
Every setting (keys, the CDN base of each region, the paths of your own data and tools) comes from the config file,
|
|
4
|
+
the environment or the flags below; see config.py and nnnotes.example.toml. JSON summaries are printed as UTF-8
|
|
5
|
+
whatever the console encoding.
|
|
6
|
+
|
|
7
|
+
nnnotes catalog --prefix Spot/ --limit 40
|
|
8
|
+
nnnotes browse [--port 8000]
|
|
9
|
+
nnnotes pull <key> [<key> ...]
|
|
10
|
+
nnnotes servers [--show-hosts]
|
|
11
|
+
nnnotes master version
|
|
12
|
+
nnnotes master decode <dir or .bin files> -o <out dir>
|
|
13
|
+
nnnotes master download --version <master version> | --latest -o <dir>
|
|
14
|
+
nnnotes adv 10462 -o out/adv_10462.json
|
|
15
|
+
nnnotes story 10462 -o out/story_10462
|
|
16
|
+
nnnotes live2d <Character/Live2D/.../model/... | model id> -o out/live2d/x
|
|
17
|
+
nnnotes spot 10001 -o out/spot_10001
|
|
18
|
+
nnnotes room <Spot/.../Background/...> -o out/room.glb
|
|
19
|
+
nnnotes shader --key <key> | --apk-bundle <substring> -o out/shaders
|
|
20
|
+
nnnotes audio <cueSheet> -o out/audio
|
|
21
|
+
nnnotes voices list [--from out/assets] [--character <id | name>] [--category <category>] [--source <source>]
|
|
22
|
+
[--episode <adv id | asset>]
|
|
23
|
+
nnnotes voices search <text> [--from out/assets] [--language ja] [--character ...] [--category ...]
|
|
24
|
+
nnnotes voices summary [--from out/assets]
|
|
25
|
+
nnnotes voices get <row id | sound id> -o out/voice [--from out/assets] [--format flac]
|
|
26
|
+
nnnotes crikey [--write <dir>]
|
|
27
|
+
nnnotes player -o out/player.json
|
|
28
|
+
nnnotes live 100001 --difficulty expert [--band 1 | --leader-card <MasterMemberCard id>] [--fonts game]
|
|
29
|
+
-o out/live_100001
|
|
30
|
+
nnnotes web out/site --player <ournotes-player> [--pair 100001:expert [--pair ...] | --all] [--format aac]
|
|
31
|
+
[--live2d <model id | key> [--live2d ...] | --all-live2d]
|
|
32
|
+
[--story 10462 [--story ...] | --all-stories] [--story-languages en,ja] [--font en=<file>]
|
|
33
|
+
[--font emoji=<file>]
|
|
34
|
+
[--region <region> [--region ...] | --all-regions]
|
|
35
|
+
nnnotes deck-data --master-files <master download dir> | --apk-master -o out/deck-data.json[.gz]
|
|
36
|
+
nnnotes export -o out/assets [--select group:<group> | key:<prefix> | bundle:<glob> ...] [--layout original,cas]
|
|
37
|
+
nnnotes plan [--select ...] [--json] [--check] [--emit-tasks <dir>]
|
|
38
|
+
nnnotes run-stage <task.json> [...]
|
|
39
|
+
nnnotes catalogs list | import <catalog.bin> | fetch | diff <old> <new>
|
|
40
|
+
nnnotes store verify
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
# numpy's OpenBLAS starts one busy thread per CPU in every process that imports it, and no export uses its
|
|
51
|
+
# parallelism. Set when the command line loads, before a command imports numpy; the worker processes of a build
|
|
52
|
+
# inherit it.
|
|
53
|
+
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
|
|
54
|
+
|
|
55
|
+
from . import __version__, cli_assets, voices
|
|
56
|
+
from .addressables import BundleKey
|
|
57
|
+
from .catalog import Catalog
|
|
58
|
+
from .config import Config, ConfigError, use
|
|
59
|
+
from .jsonio import dumps, write_json
|
|
60
|
+
from .webaudio import DEFAULT_AUDIO_FORMAT, WEB_AUDIO
|
|
61
|
+
|
|
62
|
+
DIFFICULTY_CHOICES = ("easy", "normal", "hard", "expert")
|
|
63
|
+
AUDIO_CHOICES = ("flac", "ogg", "wav")
|
|
64
|
+
# command-line flags of settings: (section, key) -> (argparse dest, flag)
|
|
65
|
+
FLAG_SETTINGS = {
|
|
66
|
+
("catalog", "region"): ("region", "--region"),
|
|
67
|
+
("catalog", "language"): ("language", "--language"),
|
|
68
|
+
("paths", "catalog"): ("catalog", "--catalog"),
|
|
69
|
+
("paths", "cache"): ("cache", "--cache"),
|
|
70
|
+
("paths", "master"): ("master", "--master"),
|
|
71
|
+
("paths", "apk"): ("apk", "--apk"),
|
|
72
|
+
("paths", "ffmpeg"): ("ffmpeg", "--ffmpeg"),
|
|
73
|
+
("paths", "vgmstream"): ("vgmstream", "--vgmstream"),
|
|
74
|
+
("paths", "node"): ("node", "--node"),
|
|
75
|
+
("paths", "player"): ("player", "--player"),
|
|
76
|
+
("paths", "store"): ("store", "--store"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------- settings -> data
|
|
81
|
+
def load_config(args) -> Config:
|
|
82
|
+
overrides = {k: getattr(args, dest, None) for k, (dest, _) in FLAG_SETTINGS.items()}
|
|
83
|
+
return use(Config.load(getattr(args, "config", None), overrides=overrides,
|
|
84
|
+
flags={k: flag for k, (_, flag) in FLAG_SETTINGS.items()}))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def bundle_key(cfg: Config) -> BundleKey:
|
|
88
|
+
return BundleKey(cfg.hex("bundle", "key", 16), cfg.hex("bundle", "nonce_seed"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _existing(cfg: Config, section: str, key: str, kind: str = "file") -> Path | None:
|
|
92
|
+
p = cfg.path(section, key)
|
|
93
|
+
if p is not None and not (p.is_file() if kind == "file" else p.is_dir()):
|
|
94
|
+
raise ConfigError(f"setting {section}.{key}: {kind} {p} not found")
|
|
95
|
+
return p
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def open_catalog(cfg: Config, bundles: bool = True, region: str | None = None) -> Catalog:
|
|
99
|
+
"""The catalog of [catalog] language (merged with the APK's when [paths] apk is set), fetching from the CDN of
|
|
100
|
+
`region` (default: [catalog] region); the regions serve the same catalog for a language, so one cached file
|
|
101
|
+
serves them all. `bundles`: bundles will be fetched; else only the catalog is read. The region, its CDN base
|
|
102
|
+
and the bundle key are read from the settings only when something must be downloaded (every file in the cache:
|
|
103
|
+
none of them is needed), then a missing one is a ConfigError naming the setting."""
|
|
104
|
+
cache = cfg.require_path("paths", "cache")
|
|
105
|
+
catbin = _existing(cfg, "paths", "catalog")
|
|
106
|
+
apk = _existing(cfg, "paths", "apk")
|
|
107
|
+
language = cfg.require("catalog", "language") if catbin is None else None
|
|
108
|
+
cdn = (lambda: cfg.cdn(region or cfg.region())) if bundles or catbin is None else None
|
|
109
|
+
key = (lambda: bundle_key(cfg)) if bundles else None
|
|
110
|
+
if catbin is not None:
|
|
111
|
+
return Catalog(catbin.read_bytes(), cache, cdn=cdn, bundle_key=key, apk=apk)
|
|
112
|
+
return Catalog.load(language, cache, cdn=cdn, bundle_key=key, apk=apk)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def master_dir(cfg: Config, region: str | None = None) -> Path:
|
|
116
|
+
"""The decoded master data of `region` (default: [catalog] region when it is set): the --master flag, else
|
|
117
|
+
[servers.<region>] master, else [paths] master."""
|
|
118
|
+
section, key = cfg.master(region or cfg.get("catalog", "region"))
|
|
119
|
+
cfg.require_path(section, key)
|
|
120
|
+
return _existing(cfg, section, key, "directory")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def player_data(cfg: Config):
|
|
124
|
+
from .player import PlayerData
|
|
125
|
+
cfg.require_path("paths", "apk")
|
|
126
|
+
return PlayerData(_existing(cfg, "paths", "apk"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def master_key(cfg: Config):
|
|
130
|
+
from .master import MasterKey
|
|
131
|
+
return MasterKey(cfg.hex("master", "key", 32), cfg.hex("master", "iv", 32))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def known_keys(args, cat: Catalog, keys) -> None:
|
|
135
|
+
"""A usage error (exit 2) naming the keys the catalog does not have."""
|
|
136
|
+
unknown = [k for k in keys if not cat.has(k)]
|
|
137
|
+
if unknown:
|
|
138
|
+
args.usage(f"not a key of the catalog: {', '.join(unknown)}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def known_row(args, md: Path, table: str, row_id: int, what: str) -> None:
|
|
142
|
+
"""A usage error (exit 2) naming `row_id` when the master table `table` has no row with that `_id`."""
|
|
143
|
+
from .master import has_row
|
|
144
|
+
try:
|
|
145
|
+
found = has_row(md, table, row_id)
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
raise ConfigError(f"master data {md}: no {table}.json (a directory written by `nnnotes master decode`)") \
|
|
148
|
+
from None
|
|
149
|
+
if not found:
|
|
150
|
+
args.usage(f"{what} {row_id}: no {table} row with this id")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _print_json(r) -> None:
|
|
154
|
+
"""Print a JSON summary as UTF-8 bytes (a GBK / cp932 console cannot encode every string)."""
|
|
155
|
+
sys.stdout.flush()
|
|
156
|
+
sys.stdout.buffer.write(dumps(r, ensure_ascii=False, indent=1).encode("utf-8") + b"\n")
|
|
157
|
+
sys.stdout.buffer.flush()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------- commands
|
|
161
|
+
def cmd_catalog(args, cfg):
|
|
162
|
+
cat = open_catalog(cfg, bundles=False)
|
|
163
|
+
ks = cat.keys(args.prefix)
|
|
164
|
+
for k in ks[: args.limit]:
|
|
165
|
+
print(k)
|
|
166
|
+
print(f"# {len(ks)} keys", file=sys.stderr)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_browse(args, cfg):
|
|
170
|
+
from .addressables import Region, serve
|
|
171
|
+
names = cfg.regions()
|
|
172
|
+
if not names:
|
|
173
|
+
raise ConfigError("no region configured: add a [servers.<region>] table with `cdn` to the config file "
|
|
174
|
+
"(or set NNNOTES_SERVERS_<REGION>_CDN)")
|
|
175
|
+
regions = []
|
|
176
|
+
for r in names:
|
|
177
|
+
langs = cfg.get_list(f"servers.{r}", "languages")
|
|
178
|
+
if not langs:
|
|
179
|
+
raise cfg.missing(f"servers.{r}", "languages")
|
|
180
|
+
regions.append(Region(r, cfg.get(f"servers.{r}", "name") or r, cfg.cdn(r), langs))
|
|
181
|
+
serve(regions, bundle_key(cfg), cfg.require_path("paths", "cache"), args.port, args.host)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_pull(args, cfg):
|
|
185
|
+
cat = open_catalog(cfg)
|
|
186
|
+
known_keys(args, cat, args.keys)
|
|
187
|
+
for key in args.keys:
|
|
188
|
+
for p in cat.fetch_key(key):
|
|
189
|
+
print(p)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cmd_master_decode(args, cfg):
|
|
193
|
+
from . import master
|
|
194
|
+
key = master_key(cfg)
|
|
195
|
+
files = master.input_files(args.inputs)
|
|
196
|
+
if not files:
|
|
197
|
+
raise SystemExit("nnnotes: no master data files (*.bin) among the inputs")
|
|
198
|
+
r = master.decode_files(files, Path(args.out), key, workers=args.workers)
|
|
199
|
+
_print_json(r)
|
|
200
|
+
if r["failed"]:
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def cmd_servers(args, cfg):
|
|
205
|
+
from . import gameapi
|
|
206
|
+
try:
|
|
207
|
+
servers = gameapi.server_list(cfg)
|
|
208
|
+
except gameapi.GameApiError as e:
|
|
209
|
+
sys.exit(f"nnnotes: {e}")
|
|
210
|
+
configured = gameapi.configured_roots(cfg)
|
|
211
|
+
_print_json({"servers": [gameapi.server_summary(s, configured, args.show_hosts) for s in servers]})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_master_version(args, cfg):
|
|
215
|
+
from . import gameapi
|
|
216
|
+
region = cfg.region()
|
|
217
|
+
try:
|
|
218
|
+
v = gameapi.master_version(cfg, region)
|
|
219
|
+
except gameapi.GameApiError as e:
|
|
220
|
+
sys.exit(f"nnnotes: {e}")
|
|
221
|
+
_print_json({"region": region, "masterVersion": v.version, "resourceVersion": v.resource_version})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_master_download(args, cfg):
|
|
225
|
+
from . import gameapi, master
|
|
226
|
+
region = cfg.region()
|
|
227
|
+
cdn = cfg.cdn(region)
|
|
228
|
+
try:
|
|
229
|
+
version = gameapi.master_version(cfg, region).version if args.latest else args.version
|
|
230
|
+
r = master.download(cdn, version, Path(args.out), workers=args.workers)
|
|
231
|
+
except (gameapi.GameApiError, master.DownloadError) as e:
|
|
232
|
+
sys.exit(f"nnnotes: {e}")
|
|
233
|
+
_print_json(r)
|
|
234
|
+
if r["failed"]:
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def cmd_adv(args, cfg):
|
|
239
|
+
from . import adv
|
|
240
|
+
md = master_dir(cfg)
|
|
241
|
+
known_row(args, md, "MasterAdv", args.adv_id, "episode")
|
|
242
|
+
cat = open_catalog(cfg)
|
|
243
|
+
doc = adv.to_json(adv.extract(cat, md, args.adv_id))
|
|
244
|
+
out = Path(args.out)
|
|
245
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
write_json(out, doc)
|
|
247
|
+
print(f"{out} ({doc['commandCount']} commands, {len(doc['resources'])} resources)")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cmd_live2d(args, cfg):
|
|
251
|
+
from . import live2d, webmodel
|
|
252
|
+
cfg.require_path("paths", "apk") # the Cubism component classes are read from the APK
|
|
253
|
+
cat = open_catalog(cfg)
|
|
254
|
+
try: # a model id or a model key
|
|
255
|
+
(key,) = webmodel.catalog_models(cat, [args.key]).values()
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
args.usage(str(e))
|
|
258
|
+
r = live2d.extract_model(cat, key, Path(args.out))
|
|
259
|
+
print(dumps(r, ensure_ascii=False))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cmd_spot(args, cfg):
|
|
263
|
+
from . import spot, room, shader
|
|
264
|
+
cfg.require_path("paths", "apk") # the Spot / Spine component classes are read from the APK
|
|
265
|
+
md = master_dir(cfg)
|
|
266
|
+
known_row(args, md, "MasterHomeSpot", args.spot_id, "spot")
|
|
267
|
+
cat = open_catalog(cfg)
|
|
268
|
+
out = Path(args.out)
|
|
269
|
+
doc = spot.extract(cat, md, args.spot_id, out)
|
|
270
|
+
print(f"{out}/spot.json ({len(doc['characters'])} tap targets, {len(doc['skeletons'])} skeletons)")
|
|
271
|
+
bg = doc["master"]["_backgroundAssetPath"]
|
|
272
|
+
r = room.extract_room(cat, bg, out / "room.glb")
|
|
273
|
+
print(f"{out}/room.glb ({r['meshCount']} meshes, {r['inactiveMeshes']} inactive)")
|
|
274
|
+
bundles = cat.fetch_key(bg) + cat.fetch_key(doc["master"]["_situationAssetPath"])
|
|
275
|
+
s = shader.dump(bundles, out / "shaders")
|
|
276
|
+
print(f"{out}/shaders/ ({s['count']} shaders, {s['variants']} variants)")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def cmd_room(args, cfg):
|
|
280
|
+
from . import room
|
|
281
|
+
cat = open_catalog(cfg)
|
|
282
|
+
known_keys(args, cat, [args.key])
|
|
283
|
+
r = room.extract_room(cat, args.key, Path(args.out))
|
|
284
|
+
print(f"{r['glb']} ({r['meshCount']} meshes, {r['inactiveMeshes']} inactive)")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_shader(args, cfg):
|
|
288
|
+
from . import shader
|
|
289
|
+
cat = open_catalog(cfg)
|
|
290
|
+
if args.key:
|
|
291
|
+
known_keys(args, cat, [args.key])
|
|
292
|
+
bundles = cat.fetch_key(args.key)
|
|
293
|
+
else:
|
|
294
|
+
if cat.apk is None:
|
|
295
|
+
cfg.require_path("paths", "apk")
|
|
296
|
+
bundles = []
|
|
297
|
+
for s in args.apk_bundle:
|
|
298
|
+
try:
|
|
299
|
+
bundles.append(cat.apk_bundle(s))
|
|
300
|
+
except KeyError as e: # none or several bundles match
|
|
301
|
+
args.usage(f"--apk-bundle {e.args[0]}")
|
|
302
|
+
r = shader.dump(bundles, Path(args.out))
|
|
303
|
+
print(f"{r['count']} shaders, {r['variants']} variants")
|
|
304
|
+
for n in r["names"]:
|
|
305
|
+
print(f" {n}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmd_audio(args, cfg):
|
|
309
|
+
from . import cri
|
|
310
|
+
flac = _flac_options(args)
|
|
311
|
+
cfg.require_path("paths", "apk") # the HCA keycode is read from the APK
|
|
312
|
+
cat = open_catalog(cfg)
|
|
313
|
+
if not cat.has(f"Cri/Sound/{args.cue_sheet}"):
|
|
314
|
+
args.usage(f"cue sheet {args.cue_sheet}: no key Cri/Sound/{args.cue_sheet} in the catalog")
|
|
315
|
+
out = Path(args.out)
|
|
316
|
+
r = cri.decode(cat, args.cue_sheet, out, fmt=args.format, **flac)
|
|
317
|
+
print(f"{out} ({len(r)} cues)")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def cmd_crikey(args, cfg):
|
|
321
|
+
from . import crikey
|
|
322
|
+
cfg.require_path("paths", "apk")
|
|
323
|
+
k = crikey.find_key(_existing(cfg, "paths", "apk"))
|
|
324
|
+
print(f"HCA keycode found ({len(str(k))} digits)" if k else "no HCA keycode (decryption disabled)")
|
|
325
|
+
if args.write and k:
|
|
326
|
+
d = Path(args.write)
|
|
327
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
print(crikey.write_hcakey(k, d))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_player(args, cfg):
|
|
332
|
+
g = player_data(cfg).graphics()
|
|
333
|
+
out = Path(args.out)
|
|
334
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
write_json(out, g)
|
|
336
|
+
print(f"{out} ({g['colorSpace']}, {len(g['qualityLevels'])} quality levels, "
|
|
337
|
+
f"{len(g['renderers'])} renderers)")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def cmd_story(args, cfg):
|
|
341
|
+
from . import languages, story
|
|
342
|
+
if args.fonts == "game":
|
|
343
|
+
from .tmpfont import require_extra
|
|
344
|
+
require_extra("--fonts game")
|
|
345
|
+
flac = _flac_options(args)
|
|
346
|
+
languages.check(cfg.require("catalog", "language")) # the story UI's language (advui reads the setting)
|
|
347
|
+
md = master_dir(cfg)
|
|
348
|
+
known_row(args, md, "MasterAdv", args.adv_id, "episode")
|
|
349
|
+
cat = open_catalog(cfg)
|
|
350
|
+
r = story.build(cat, md, player_data(cfg), args.adv_id, Path(args.out),
|
|
351
|
+
audio_format=args.format, audio=not args.no_audio, fonts=args.fonts, audio_options=flac)
|
|
352
|
+
_print_json(r)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _flac_options(args) -> dict:
|
|
356
|
+
"""The cri.decode options of --flac-level ({} without it); a level out of range, or the flag where no FLAC is
|
|
357
|
+
written, is a usage error."""
|
|
358
|
+
level = args.flac_level
|
|
359
|
+
if level is None:
|
|
360
|
+
return {}
|
|
361
|
+
if not 0 <= level <= 12:
|
|
362
|
+
args.usage(f"--flac-level {level}: expected 0 to 12")
|
|
363
|
+
if args.format != "flac":
|
|
364
|
+
args.usage(f"--flac-level: --format {args.format} writes no FLAC")
|
|
365
|
+
if getattr(args, "no_audio", False):
|
|
366
|
+
args.usage("--flac-level: --no-audio decodes no audio")
|
|
367
|
+
return {"flac_level": level}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _fonts_extra(fonts: str) -> None:
|
|
371
|
+
"""`--fonts game` needs the optional `fonts` dependencies."""
|
|
372
|
+
if fonts == "game":
|
|
373
|
+
from .tmpfont import require_extra
|
|
374
|
+
require_extra("--fonts game")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _live_options(args) -> dict:
|
|
378
|
+
"""The --live-option specs as a request (liveoptions.parse_specs); a malformed spec is a usage error."""
|
|
379
|
+
from .liveoptions import OptionSpecError, parse_specs
|
|
380
|
+
try:
|
|
381
|
+
return parse_specs(args.live_option)
|
|
382
|
+
except OptionSpecError as e:
|
|
383
|
+
args.usage(str(e))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cmd_live(args, cfg):
|
|
387
|
+
from . import languages, live, liveoptions
|
|
388
|
+
from .web import all_pairs
|
|
389
|
+
_fonts_extra(args.fonts)
|
|
390
|
+
request = _live_options(args)
|
|
391
|
+
flac = _flac_options(args)
|
|
392
|
+
language = languages.check(cfg.require("catalog", "language"))
|
|
393
|
+
md = master_dir(cfg)
|
|
394
|
+
known_row(args, md, "MasterLiveMusic", args.music_id, "music")
|
|
395
|
+
if (args.music_id, args.difficulty) not in all_pairs(md):
|
|
396
|
+
args.usage(f"music {args.music_id}: no {args.difficulty} chart (no MasterLiveMusicScore row)")
|
|
397
|
+
try:
|
|
398
|
+
options = liveoptions.resolve(request, md)
|
|
399
|
+
except liveoptions.OptionSpecError as e:
|
|
400
|
+
args.usage(str(e))
|
|
401
|
+
cat = open_catalog(cfg)
|
|
402
|
+
try: # an unknown --leader-card, a --band without its scene keys
|
|
403
|
+
live.resolve_band(cat, md, args.music_id, band=args.band, leader_card=args.leader_card)
|
|
404
|
+
except (KeyError, ValueError) as e:
|
|
405
|
+
args.usage(e.args[0] if e.args else type(e).__name__)
|
|
406
|
+
r = live.build(cat, md, player_data(cfg), args.music_id, args.difficulty, Path(args.out),
|
|
407
|
+
audio_format=args.format, band=args.band, leader_card=args.leader_card, language=language,
|
|
408
|
+
fonts=args.fonts, options=options, **flac)
|
|
409
|
+
_print_json(r)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def cmd_web(args, cfg):
|
|
413
|
+
from . import liveoptions, storysite, web, webmodel
|
|
414
|
+
live_options = _live_options(args)
|
|
415
|
+
charts, models = bool(args.pair or args.all), bool(args.live2d or args.all_live2d)
|
|
416
|
+
stories = bool(args.story or args.all_stories)
|
|
417
|
+
if not (charts or models or stories or args.player_only or args.reingest_json):
|
|
418
|
+
args.usage("one of the arguments --pair --all --live2d --all-live2d --story --all-stories --player-only "
|
|
419
|
+
"--reingest-json is required")
|
|
420
|
+
if models and (args.player_only or args.reingest_json):
|
|
421
|
+
args.usage("--player-only and --reingest-json build nothing: leave out --live2d / --all-live2d")
|
|
422
|
+
if stories and (args.player_only or args.reingest_json):
|
|
423
|
+
args.usage("--player-only and --reingest-json build nothing: leave out --story / --all-stories")
|
|
424
|
+
if live_options and not charts:
|
|
425
|
+
args.usage("--live-option applies to charts: give --pair or --all")
|
|
426
|
+
player = cfg.path("paths", "player")
|
|
427
|
+
out = Path(args.out)
|
|
428
|
+
if args.player_only or args.reingest_json:
|
|
429
|
+
r = {**(web.reingest_json(out) if args.reingest_json else {}),
|
|
430
|
+
**web.write_player(out, web.check_player(player)), **web.write_index(out)}
|
|
431
|
+
else:
|
|
432
|
+
web.check_player(player)
|
|
433
|
+
regions = (web.site_regions(cfg, args.web_regions, args.all_regions)
|
|
434
|
+
if args.web_regions or args.all_regions else None) # None: the one [catalog] region
|
|
435
|
+
base = {"region": regions[0]} if regions else {}
|
|
436
|
+
unknown = web.unknown_pairs(cfg, args.pair, regions) if args.pair else []
|
|
437
|
+
if unknown:
|
|
438
|
+
args.usage(f"chart{'s' if len(unknown) > 1 else ''} {', '.join(f'{m}:{d}' for m, d in unknown)}: no "
|
|
439
|
+
f"MasterLiveMusicScore row in the master data of the site's region(s)")
|
|
440
|
+
if live_options: # a value a region's master data does not have
|
|
441
|
+
try:
|
|
442
|
+
for md in web.region_masters(cfg, web.site_regions(cfg, regions)).values():
|
|
443
|
+
liveoptions.resolve(live_options, md)
|
|
444
|
+
except liveoptions.OptionSpecError as e:
|
|
445
|
+
args.usage(str(e))
|
|
446
|
+
r = {}
|
|
447
|
+
if models:
|
|
448
|
+
cfg.require_path("paths", "apk") # the Cubism component classes and mask materials: the APK
|
|
449
|
+
try:
|
|
450
|
+
selected = webmodel.catalog_models(open_catalog(cfg, bundles=False, **base), args.live2d)
|
|
451
|
+
except ValueError as e:
|
|
452
|
+
args.usage(str(e))
|
|
453
|
+
r.update(webmodel.build(out, selected, cfg, player, force=args.force, tmp_dir=args.tmp,
|
|
454
|
+
workers=args.workers, **base))
|
|
455
|
+
if charts:
|
|
456
|
+
_fonts_extra(args.fonts)
|
|
457
|
+
r.update(web.build(out, None if args.all else args.pair, cfg, player, args.format,
|
|
458
|
+
audio=not args.no_audio, force=args.force, tmp_dir=args.tmp, workers=args.workers,
|
|
459
|
+
band=args.band, leader_card=args.leader_card, regions=regions, fonts=args.fonts,
|
|
460
|
+
read_workers=args.read_workers, live_options=live_options))
|
|
461
|
+
if stories:
|
|
462
|
+
from .tmpfont import require_extra
|
|
463
|
+
require_extra("--story / --all-stories") # the font assets of the story text (open and game)
|
|
464
|
+
cfg.require_path("paths", "apk") # the ADV settings and UI are embedded content of the APK
|
|
465
|
+
unknown = storysite.unknown_stories(cfg, args.story, regions) if args.story else []
|
|
466
|
+
if unknown:
|
|
467
|
+
noun = "stories" if len(unknown) > 1 else "story"
|
|
468
|
+
args.usage(f"{noun} {', '.join(map(str, unknown))}: no MasterAdv row in the master data of the "
|
|
469
|
+
f"site's region(s)")
|
|
470
|
+
r.update(storysite.build(out, args.story, cfg, player, args.format, audio=not args.no_audio,
|
|
471
|
+
force=args.force, tmp_dir=args.tmp, workers=args.workers, regions=regions,
|
|
472
|
+
story_languages=args.story_languages, fonts=args.fonts,
|
|
473
|
+
fonts_flags=dict(args.font or [])))
|
|
474
|
+
_print_json(r)
|
|
475
|
+
if r.get("failed") or r.get("modelsFailed") or r.get("storiesFailed"):
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def cmd_deck_data(args, cfg):
|
|
480
|
+
from . import deckdata
|
|
481
|
+
if args.apk_master:
|
|
482
|
+
cfg.require_path("paths", "apk") # the master data files ship in the APK
|
|
483
|
+
apk = _existing(cfg, "paths", "apk")
|
|
484
|
+
try:
|
|
485
|
+
if args.apk_master:
|
|
486
|
+
src, region = deckdata.apk_master(apk), deckdata.EMBEDDED
|
|
487
|
+
else:
|
|
488
|
+
src, region = deckdata.master_files(Path(args.master_files)), cfg.region()
|
|
489
|
+
key = master_key(cfg)
|
|
490
|
+
cat = open_catalog(cfg)
|
|
491
|
+
r = deckdata.export(Path(args.out), src, key, deckdata.catalog_fetch(cat), region=region,
|
|
492
|
+
client=deckdata.apk_client(apk) if apk is not None else {},
|
|
493
|
+
catalog=deckdata.catalog_info(cat, cli_assets.store_root(args, cfg)))
|
|
494
|
+
except deckdata.DeckDataError as e:
|
|
495
|
+
sys.exit(f"nnnotes: {e}")
|
|
496
|
+
_print_json(r)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ---------------------------------------------------------------- parser
|
|
500
|
+
def parse_pair(s: str) -> tuple[int, str]:
|
|
501
|
+
m = re.fullmatch(r"(\d+)[:_](easy|normal|hard|expert)", s)
|
|
502
|
+
if not m:
|
|
503
|
+
raise argparse.ArgumentTypeError(f"{s}: expected <musicId>:<difficulty>")
|
|
504
|
+
return int(m.group(1)), m.group(2)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def parse_languages(s: str) -> list[str]:
|
|
508
|
+
from .languages import LANGUAGES
|
|
509
|
+
codes = [c.strip() for c in s.split(",") if c.strip()]
|
|
510
|
+
if not codes or any(c not in LANGUAGES for c in codes):
|
|
511
|
+
raise argparse.ArgumentTypeError(f"{s}: expected languages from {', '.join(LANGUAGES)}, comma-separated")
|
|
512
|
+
return codes
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
EMOJI_FONT = "emoji" # --font emoji=<file>, as storysite.EMOJI_FONT
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def parse_font(s: str) -> tuple[str, str]:
|
|
519
|
+
from .languages import LANGUAGES
|
|
520
|
+
lang, sep, path = s.partition("=")
|
|
521
|
+
if not sep or (lang not in LANGUAGES and lang != EMOJI_FONT) or not path:
|
|
522
|
+
raise argparse.ArgumentTypeError(f"{s}: expected <language>=<font file> or {EMOJI_FONT}=<emoji font file>, "
|
|
523
|
+
f"language one of {', '.join(LANGUAGES)}")
|
|
524
|
+
return lang, path
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _live_option_arg(c) -> None:
|
|
528
|
+
c.add_argument("--live-option", action="append", metavar="OPTION[=VALUES]",
|
|
529
|
+
help="also export the files of a Live option's variants (repeatable): MirrorChart, "
|
|
530
|
+
"MeasureLineDisplay, NoteDesignId, NoteEffectId, LiveQuality, NoteSePatternId (every value "
|
|
531
|
+
"of the master data, or =v,v...), defaults (the option defaults and ranges only) or all")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _band_args(c) -> None:
|
|
535
|
+
g = c.add_mutually_exclusive_group()
|
|
536
|
+
g.add_argument("--band", type=int, help="band of the LightWeight background and start timeline "
|
|
537
|
+
"(default: band of the music's first vocal character)")
|
|
538
|
+
g.add_argument("--leader-card", type=int, help="deck centre MasterMemberCard id (its character's band)")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _fonts_arg(c) -> None:
|
|
542
|
+
c.add_argument("--fonts", default="open", choices=("open", "game"),
|
|
543
|
+
help="start canvas text: open: layout and style only, no font data (default); game: also the "
|
|
544
|
+
"game's TMP fonts (needs the 'fonts' extra)")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _audio_args(c) -> None:
|
|
548
|
+
"""--format and --flac-level of a command that decodes cue sheets."""
|
|
549
|
+
c.add_argument("--format", default="flac", choices=AUDIO_CHOICES, help="audio file format (default flac)")
|
|
550
|
+
c.add_argument("--flac-level", type=int, metavar="N",
|
|
551
|
+
help="FLAC compression level 0-12 of --format flac (default 8; every level decodes to the same "
|
|
552
|
+
"samples)")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _out(c, what: str, required: bool = True) -> None:
|
|
556
|
+
c.add_argument("-o", "--out", required=required, help=what)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
560
|
+
p = argparse.ArgumentParser(prog="nnnotes", description="BanG Dream! Our Notes data toolkit")
|
|
561
|
+
p.add_argument("--version", action="version", version=f"nnnotes {__version__}")
|
|
562
|
+
p.add_argument("--config", help="TOML config file (else NNNOTES_CONFIG, else ./nnnotes.toml)")
|
|
563
|
+
p.add_argument("--region", help="region: a [servers.<region>] table ([catalog] region)")
|
|
564
|
+
p.add_argument("--language", help="catalog and client language: ja, en, zh-Hant, zh-Hans or ko "
|
|
565
|
+
"([catalog] language)")
|
|
566
|
+
p.add_argument("--catalog", help="catalog .bin file ([paths] catalog; else downloaded into the cache)")
|
|
567
|
+
p.add_argument("--cache", help="cache directory ([paths] cache)")
|
|
568
|
+
p.add_argument("--master", help="decoded master data directory ([paths] master)")
|
|
569
|
+
p.add_argument("--apk", help="base.apk ([paths] apk)")
|
|
570
|
+
p.add_argument("--ffmpeg", help="ffmpeg executable ([paths] ffmpeg; else on PATH)")
|
|
571
|
+
p.add_argument("--vgmstream", help="vgmstream-cli executable ([paths] vgmstream; else on PATH)")
|
|
572
|
+
p.add_argument("--node", help="Node.js executable ([paths] node; else on PATH)")
|
|
573
|
+
sub = p.add_subparsers(dest="cmd", required=True, metavar="<command>")
|
|
574
|
+
|
|
575
|
+
c = sub.add_parser("catalog", help="list addressable keys")
|
|
576
|
+
c.add_argument("--prefix", default="", help="only the keys that start with this prefix")
|
|
577
|
+
c.add_argument("--limit", type=int, default=200, help="print at most N keys (default 200; the total goes "
|
|
578
|
+
"to stderr)")
|
|
579
|
+
c.set_defaults(func=cmd_catalog)
|
|
580
|
+
|
|
581
|
+
c = sub.add_parser("browse", help="browse the configured regions' catalogs and bundles in a local web page")
|
|
582
|
+
c.add_argument("--port", type=int, default=8000, help="port (default 8000)")
|
|
583
|
+
c.add_argument("--host", default="127.0.0.1", help="address to listen on (default 127.0.0.1)")
|
|
584
|
+
c.set_defaults(func=cmd_browse)
|
|
585
|
+
|
|
586
|
+
c = sub.add_parser("pull", help="fetch the bundle closure of keys into the cache")
|
|
587
|
+
c.add_argument("keys", nargs="+", metavar="KEY", help="addressable keys (`catalog` lists them)")
|
|
588
|
+
c.set_defaults(func=cmd_pull, usage=c.error)
|
|
589
|
+
|
|
590
|
+
c = sub.add_parser("servers", help="the server list of the bootstrap API root: regions, their CDN and API roots")
|
|
591
|
+
c.add_argument("--show-hosts", action="store_true", help="print the CDN and API roots, not only their counts")
|
|
592
|
+
c.set_defaults(func=cmd_servers)
|
|
593
|
+
|
|
594
|
+
c = sub.add_parser("master", help="master data files")
|
|
595
|
+
msub = c.add_subparsers(dest="master_cmd", required=True, metavar="<master command>")
|
|
596
|
+
m = msub.add_parser("version", help="the master data and resource version the region serves now (game API)")
|
|
597
|
+
m.set_defaults(func=cmd_master_version)
|
|
598
|
+
m = msub.add_parser("decode", help="master data .bin files -> <Table>.json")
|
|
599
|
+
m.add_argument("inputs", nargs="+", type=Path, help="directories (their *.bin files) or files")
|
|
600
|
+
_out(m, "output directory")
|
|
601
|
+
m.add_argument("--workers", type=int, default=8, help="parallel decodes")
|
|
602
|
+
m.set_defaults(func=cmd_master_decode)
|
|
603
|
+
m = msub.add_parser("download", help="a master data version from the region's CDN (SHA-256 checked)")
|
|
604
|
+
g = m.add_mutually_exclusive_group(required=True)
|
|
605
|
+
g.add_argument("--version", help="master data version")
|
|
606
|
+
g.add_argument("--latest", action="store_true", help="the version the region serves now (`master version`)")
|
|
607
|
+
_out(m, "directory for MasterManifest.json and the .bin files")
|
|
608
|
+
m.add_argument("--workers", type=int, default=16, help="parallel downloads")
|
|
609
|
+
m.set_defaults(func=cmd_master_download)
|
|
610
|
+
|
|
611
|
+
c = sub.add_parser("adv", help="ADV episode -> JSON")
|
|
612
|
+
c.add_argument("adv_id", type=int, help="MasterAdv id of the episode")
|
|
613
|
+
_out(c, "output .json file")
|
|
614
|
+
c.set_defaults(func=cmd_adv, usage=c.error)
|
|
615
|
+
|
|
616
|
+
c = sub.add_parser("story", help="ADV episode -> story dir (episode, models, audio, scene, UI, media, videos)")
|
|
617
|
+
c.add_argument("adv_id", type=int, help="MasterAdv id of the episode")
|
|
618
|
+
_out(c, "output directory")
|
|
619
|
+
_audio_args(c)
|
|
620
|
+
c.add_argument("--no-audio", action="store_true", help="do not decode the cue sheets")
|
|
621
|
+
c.add_argument("--fonts", default="open", choices=("open", "game"),
|
|
622
|
+
help="open: text layout and style only, no font data (default); game: also the game's TMP fonts "
|
|
623
|
+
"(needs the 'fonts' extra)")
|
|
624
|
+
c.set_defaults(func=cmd_story, usage=c.error)
|
|
625
|
+
|
|
626
|
+
c = sub.add_parser("live2d", help="Live2D model -> runtime model dir")
|
|
627
|
+
c.add_argument("key", help="model key Character/Live2D/<group>/<name>/model/<name>, or its model id <name>")
|
|
628
|
+
_out(c, "output directory")
|
|
629
|
+
c.set_defaults(func=cmd_live2d, usage=c.error)
|
|
630
|
+
|
|
631
|
+
c = sub.add_parser("spot", help="spot -> spot.json + Spine + room.glb + shaders")
|
|
632
|
+
c.add_argument("spot_id", type=int, help="MasterSpot id")
|
|
633
|
+
_out(c, "output directory")
|
|
634
|
+
c.set_defaults(func=cmd_spot, usage=c.error)
|
|
635
|
+
|
|
636
|
+
c = sub.add_parser("room", help="background prefab -> glb")
|
|
637
|
+
c.add_argument("key", help="addressable key of the prefab")
|
|
638
|
+
_out(c, "output .glb file")
|
|
639
|
+
c.set_defaults(func=cmd_room, usage=c.error)
|
|
640
|
+
|
|
641
|
+
c = sub.add_parser("shader", help="dump shaders of a key's closure or of APK bundles")
|
|
642
|
+
g = c.add_mutually_exclusive_group(required=True)
|
|
643
|
+
g.add_argument("--key", help="addressable key: the shaders of its bundle closure")
|
|
644
|
+
g.add_argument("--apk-bundle", nargs="+", help="substring(s) of APK bundle file names")
|
|
645
|
+
_out(c, "output directory")
|
|
646
|
+
c.set_defaults(func=cmd_shader, usage=c.error)
|
|
647
|
+
|
|
648
|
+
c = sub.add_parser("audio", help="decode a CRI cue sheet")
|
|
649
|
+
c.add_argument("cue_sheet", help="cue sheet name: the key Cri/Sound/<cue_sheet> of the catalog")
|
|
650
|
+
_out(c, "output directory")
|
|
651
|
+
_audio_args(c)
|
|
652
|
+
c.set_defaults(func=cmd_audio, usage=c.error)
|
|
653
|
+
|
|
654
|
+
c = sub.add_parser("crikey", help="find the HCA keycode in base.apk")
|
|
655
|
+
c.add_argument("--write", help="directory to write .hcakey into")
|
|
656
|
+
c.set_defaults(func=cmd_crikey)
|
|
657
|
+
|
|
658
|
+
c = sub.add_parser("player", help="player graphics settings -> JSON")
|
|
659
|
+
_out(c, "output .json file")
|
|
660
|
+
c.set_defaults(func=cmd_player)
|
|
661
|
+
|
|
662
|
+
c = sub.add_parser("live", help="live (music + difficulty) -> self-contained live dir")
|
|
663
|
+
c.add_argument("music_id", type=int, help="MasterLiveMusic id")
|
|
664
|
+
c.add_argument("--difficulty", default="expert", choices=DIFFICULTY_CHOICES,
|
|
665
|
+
help="chart difficulty (default expert)")
|
|
666
|
+
_audio_args(c)
|
|
667
|
+
_fonts_arg(c)
|
|
668
|
+
_band_args(c)
|
|
669
|
+
_live_option_arg(c)
|
|
670
|
+
_out(c, "output directory")
|
|
671
|
+
c.set_defaults(func=cmd_live, usage=c.error)
|
|
672
|
+
|
|
673
|
+
c = sub.add_parser("web", help="live charts, Live2D models and stories -> static site for ournotes-player "
|
|
674
|
+
"(shared player + per-chart / per-model / per-story data)")
|
|
675
|
+
c.add_argument("out", help="site directory")
|
|
676
|
+
c.add_argument("--player", help="ournotes-player checkout (built) or installed package ([paths] player)")
|
|
677
|
+
g = c.add_mutually_exclusive_group()
|
|
678
|
+
g.add_argument("--pair", type=parse_pair, action="append", help="<musicId>:<difficulty> (repeatable)")
|
|
679
|
+
g.add_argument("--all", action="store_true", help="every MasterLiveMusic x difficulty")
|
|
680
|
+
g.add_argument("--player-only", action="store_true",
|
|
681
|
+
help="rewrite the player files, charts.json and models.json only")
|
|
682
|
+
g.add_argument("--reingest-json", action="store_true", help="store every chart's and model's JSON files again")
|
|
683
|
+
m = c.add_mutually_exclusive_group()
|
|
684
|
+
m.add_argument("--live2d", action="append", metavar="MODEL",
|
|
685
|
+
help="Live2D model id or key Character/Live2D/<group>/<name>/model/<name> (repeatable)")
|
|
686
|
+
m.add_argument("--all-live2d", action="store_true", help="every Live2D model of the catalog")
|
|
687
|
+
c.add_argument("--format", default=DEFAULT_AUDIO_FORMAT, choices=tuple(WEB_AUDIO), help="BGM format")
|
|
688
|
+
c.add_argument("--no-audio", action="store_true", help="export no audio files")
|
|
689
|
+
c.add_argument("--force", action="store_true", help="rebuild charts and models whose manifest exists")
|
|
690
|
+
c.add_argument("--tmp", help="directory for the temporary live and model builds (default <site>.tmp)")
|
|
691
|
+
c.add_argument("--workers", type=int,
|
|
692
|
+
help="parallel music / model processes (default a quarter of the CPUs, up to 8 / up to 4; "
|
|
693
|
+
"1 = this process)")
|
|
694
|
+
c.add_argument("--read-workers", type=int,
|
|
695
|
+
help="chart read sets run at a time (default half the CPUs, up to 16)")
|
|
696
|
+
t = c.add_mutually_exclusive_group()
|
|
697
|
+
t.add_argument("--story", type=int, action="append", metavar="ADV_ID",
|
|
698
|
+
help="story episode: its MasterAdv id (repeatable)")
|
|
699
|
+
t.add_argument("--all-stories", action="store_true", help="every story episode (MasterAdv) of the master data")
|
|
700
|
+
c.add_argument("--story-languages", type=parse_languages, metavar="LANGS",
|
|
701
|
+
help="languages of the stories' text, comma-separated (default: ja,en,zh-Hant,zh-Hans,ko)")
|
|
702
|
+
c.add_argument("--font", type=parse_font, action="append", metavar="LANG=PATH",
|
|
703
|
+
help="font file the story text of a language is drawn with (repeatable; [paths] fonts.<lang>); "
|
|
704
|
+
"emoji=PATH: the colour emoji font the emoji sprites are drawn from ([paths] fonts.emoji)")
|
|
705
|
+
r = c.add_mutually_exclusive_group()
|
|
706
|
+
r.add_argument("--region", dest="web_regions", action="append", metavar="REGION",
|
|
707
|
+
help="a region the site serves: a [servers.<region>] table (repeatable; the first is the base; "
|
|
708
|
+
"default: [catalog] region)")
|
|
709
|
+
r.add_argument("--all-regions", action="store_true", help="every configured region")
|
|
710
|
+
c.add_argument("--fonts", default="open", choices=("open", "game"),
|
|
711
|
+
help="text of the start canvas and the stories: open (default): the start canvas has layout and "
|
|
712
|
+
"style only, the stories glyphs from your font files (--font); game: the game's TMP fonts "
|
|
713
|
+
"(needs the 'fonts' extra)")
|
|
714
|
+
_band_args(c)
|
|
715
|
+
_live_option_arg(c)
|
|
716
|
+
c.set_defaults(func=cmd_web, usage=c.error)
|
|
717
|
+
|
|
718
|
+
c = sub.add_parser("deck-data", help="every live chart and the master data tables deck-building tools read -> "
|
|
719
|
+
"one JSON file")
|
|
720
|
+
g = c.add_mutually_exclusive_group(required=True)
|
|
721
|
+
g.add_argument("--master-files", metavar="DIR",
|
|
722
|
+
help="master data files as served: MasterManifest.json and the .bin files it lists "
|
|
723
|
+
"(`master download`)")
|
|
724
|
+
g.add_argument("--apk-master", action="store_true", help="the master data files of base.apk ([paths] apk)")
|
|
725
|
+
_out(c, "output file (.json, or .json.gz for gzip)")
|
|
726
|
+
c.set_defaults(func=cmd_deck_data, usage=c.error)
|
|
727
|
+
|
|
728
|
+
cli_assets.register(sub, argparse.Namespace(open_catalog=open_catalog, print_json=_print_json))
|
|
729
|
+
voices.register(sub, argparse.Namespace(open_catalog=open_catalog, master_dir=master_dir))
|
|
730
|
+
return p
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def main(argv=None):
|
|
734
|
+
args = build_parser().parse_args(argv)
|
|
735
|
+
try:
|
|
736
|
+
cfg = load_config(args)
|
|
737
|
+
args.func(args, cfg)
|
|
738
|
+
except ConfigError as e:
|
|
739
|
+
print(f"nnnotes: {e}", file=sys.stderr)
|
|
740
|
+
sys.exit(2)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
if __name__ == "__main__":
|
|
744
|
+
main()
|