nnnotes 0.1.1__tar.gz → 0.1.2__tar.gz
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-0.1.1/src/nnnotes.egg-info → nnnotes-0.1.2}/PKG-INFO +5 -5
- {nnnotes-0.1.1 → nnnotes-0.1.2}/README.en.md +4 -4
- {nnnotes-0.1.1 → nnnotes-0.1.2}/README.md +4 -4
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/__init__.py +1 -1
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/room.py +10 -4
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/unity.py +12 -7
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/webmodel.py +44 -16
- {nnnotes-0.1.1 → nnnotes-0.1.2/src/nnnotes.egg-info}/PKG-INFO +5 -5
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_room.py +33 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_speed_exact.py +13 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_webmodel.py +41 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/LICENSE +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/pyproject.toml +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/setup.cfg +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/__main__.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/acb.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/addressables.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/adv.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advcommand.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advmedia.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advscene.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advui.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advvideo.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/__init__.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/astc.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/gltf.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/mesh.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/png.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/reader.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/sniff.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/sprite.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/texture.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cache.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/catalog.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/catalogdb.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/census.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cli.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cli_assets.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/compress.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/config.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/configfile.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/contract.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cri.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/crikey.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/crilips.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cristages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/deckdata.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/export.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/gameapi.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/jsonio.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/languages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/layout.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/link.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/live.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/live2d.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveaudio.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/livenotes.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveoptions.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/livescene.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveui.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/master.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/modelstages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/motion.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/nnnotes.example.toml +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/objexport.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/orchestrate.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/plan.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/player.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/score.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/shader.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/spot.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/stages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/store.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/story.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyfonts.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyhost.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storysite.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyvoices.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/textstyle.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/tmpfont.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/typetrees/6000.3.12f1.json +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/viewrules.json +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/views.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/voices.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/web.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/webaudio.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/SOURCES.txt +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/dependency_links.txt +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/entry_points.txt +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/requires.txt +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/top_level.txt +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_advvideo.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atom_eval.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_mesh.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_sprite.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_texture.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_blob_memo.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cache.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalog.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalog_lazy.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalogdb.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_census.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cli.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cli_assets.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_compress.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_config.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_configfile.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_contract.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_crilips.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cristages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_deckdata.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_gameapi.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_goldens.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_jsonio.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_layout.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_link.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_live_options.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_live_options_web.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_master.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_mesh_texture_guards.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_modelstages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_objexport.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_orchestrate.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_pipeline.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_plan.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_player.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_readsets.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_regions.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_repo.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_sniff.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_spot.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_stages.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_store.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_closure.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_fonts.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_host.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_kinds.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_site.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_ui.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_textstyle.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_typetrees.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_views.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_voices.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_voices_story.py +0 -0
- {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnnotes
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Offline data toolkit for BanG Dream! Our Notes game files: catalogs, bundles, master data, stories, Live2D, audio and live charts
|
|
5
5
|
Author: MetaMiku, emptysekai
|
|
6
6
|
License-Expression: MIT
|
|
@@ -105,13 +105,13 @@ Based on all data of the Taiwan server, version 1.0.1 (zh-Hant):
|
|
|
105
105
|
| catalog / bundle decryption / dependency closure | working |
|
|
106
106
|
| master data decoding | working |
|
|
107
107
|
| stories, `adv` | 946 / 946 episodes |
|
|
108
|
-
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list);
|
|
108
|
+
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list); 946 / 946 exported and checked one by one: the files, models and audio the output refers to exist, every JSON parses and every path it refers to resolves; the 235 episodes with frames, effects, post effects, stills, chats, talk windows or videos are byte-identical across two exports except for the models directory path |
|
|
109
109
|
| Live2D models | 239 / 239 (every model of the catalog; episodes use 185 of them) |
|
|
110
110
|
| CRI audio | 681 / 681 cue sheets |
|
|
111
111
|
| charts, `live` | 336 / 336 (music, difficulty) pairs |
|
|
112
|
-
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation;
|
|
113
|
-
| spots, `spot` / `room` |
|
|
114
|
-
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is
|
|
112
|
+
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation); two full builds of the three regions in five languages with AAC audio (`--all-regions`, with the live master data: 340 charts of 85 musics) are likewise byte-identical and pass the data validation in full |
|
|
113
|
+
| spots, `spot` / `room` | 39 / 39 spots, every reference resolves; `room` exports 20 / 20 room backgrounds of the catalog. Every glb passes the Khronos glTF-Validator with 0 errors |
|
|
114
|
+
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is verified (the row above) |
|
|
115
115
|
|
|
116
116
|
## Requirements
|
|
117
117
|
|
|
@@ -74,13 +74,13 @@ Based on all data of the Taiwan server, version 1.0.1 (zh-Hant):
|
|
|
74
74
|
| catalog / bundle decryption / dependency closure | working |
|
|
75
75
|
| master data decoding | working |
|
|
76
76
|
| stories, `adv` | 946 / 946 episodes |
|
|
77
|
-
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list);
|
|
77
|
+
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list); 946 / 946 exported and checked one by one: the files, models and audio the output refers to exist, every JSON parses and every path it refers to resolves; the 235 episodes with frames, effects, post effects, stills, chats, talk windows or videos are byte-identical across two exports except for the models directory path |
|
|
78
78
|
| Live2D models | 239 / 239 (every model of the catalog; episodes use 185 of them) |
|
|
79
79
|
| CRI audio | 681 / 681 cue sheets |
|
|
80
80
|
| charts, `live` | 336 / 336 (music, difficulty) pairs |
|
|
81
|
-
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation;
|
|
82
|
-
| spots, `spot` / `room` |
|
|
83
|
-
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is
|
|
81
|
+
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation); two full builds of the three regions in five languages with AAC audio (`--all-regions`, with the live master data: 340 charts of 85 musics) are likewise byte-identical and pass the data validation in full |
|
|
82
|
+
| spots, `spot` / `room` | 39 / 39 spots, every reference resolves; `room` exports 20 / 20 room backgrounds of the catalog. Every glb passes the Khronos glTF-Validator with 0 errors |
|
|
83
|
+
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is verified (the row above) |
|
|
84
84
|
|
|
85
85
|
## Requirements
|
|
86
86
|
|
|
@@ -59,13 +59,13 @@ nnnotes config check # 每项设置的来源和格式是否有效,
|
|
|
59
59
|
| catalog / 资源包解密 / 依赖闭包 | 可用 |
|
|
60
60
|
| masterdata 解码 | 可用 |
|
|
61
61
|
| 剧情 `adv` | 946 / 946 集可导出 |
|
|
62
|
-
| 剧情 `story`(完整目录) | 946 / 946 集的资源都在 catalog
|
|
62
|
+
| 剧情 `story`(完整目录) | 946 / 946 集的资源都在 catalog 中且类型均受支持(资源闭包与游戏自带的每集下载清单一致);946 / 946 集已逐集导出验证:输出中引用的文件、模型与音频均存在,JSON 均可解析,引用的路径均可解析;其中 235 集用到 Frame / Effect / PostEffect / Still / Chat / TalkWindow / 视频资源,两次导出除模型目录路径外逐字节一致 |
|
|
63
63
|
| Live2D 模型 | 239 / 239 个可导出(catalog 中的全部模型,剧情用到其中 185 个) |
|
|
64
64
|
| CRI 音频 | 681 / 681 个 cue sheet 可解码 |
|
|
65
65
|
| 谱面 `live` | 336 / 336 个(曲目, 难度)组合可导出 |
|
|
66
|
-
| 网页站点 `web` | 336 / 336 张谱面,239 / 239 个 Live2D 模型,946 / 946 集剧情(英语、无音频的全量构建两次结果逐字节一致,946 / 946
|
|
67
|
-
| 据点 `spot` / `room` |
|
|
68
|
-
| 其他区服(en / kr)与其他语言 | 已核对:各区服同一语言的 catalog 相同、资源包相同,密钥通用;谱面用到的 masterdata
|
|
66
|
+
| 网页站点 `web` | 336 / 336 张谱面,239 / 239 个 Live2D 模型,946 / 946 集剧情(英语、无音频的全量构建两次结果逐字节一致,946 / 946 集通过数据校验);三个区服、五种语言、含 AAC 音频的全量构建(`--all-regions`,用线上 masterdata:85 首曲目的 340 张谱面)两次结果同样逐字节一致,全部通过数据校验 |
|
|
67
|
+
| 据点 `spot` / `room` | 39 / 39 个据点可导出,引用均可解析;catalog 中 20 / 20 个房间背景可由 `room` 导出。生成的 glb 均通过 Khronos glTF-Validator,0 个错误 |
|
|
68
|
+
| 其他区服(en / kr)与其他语言 | 已核对:各区服同一语言的 catalog 相同、资源包相同,密钥通用;谱面用到的 masterdata 表在三个区服间一致,文本表五种语言齐全;抽查的谱面导出与台服一致。多区服整站已全量构建验证(见上一行) |
|
|
69
69
|
|
|
70
70
|
## 需要准备
|
|
71
71
|
|
|
@@ -4,4 +4,4 @@ Reads Addressables catalogs, fetches and decrypts asset bundles, decodes master
|
|
|
4
4
|
models, spots, shaders, CRI audio and live charts as JSON and common file formats. Game files, keys and server
|
|
5
5
|
addresses are supplied by the user through the configuration; nothing of the game is included.
|
|
6
6
|
"""
|
|
7
|
-
__version__ = "0.1.
|
|
7
|
+
__version__ = "0.1.2"
|
|
@@ -258,23 +258,29 @@ class _Glb:
|
|
|
258
258
|
"extras": {"unityActive": active}})
|
|
259
259
|
|
|
260
260
|
def write(self, path: Path, extras: dict):
|
|
261
|
+
"""glTF requires the arrays it has to be non-empty and a buffer to hold bytes: an empty array is left out,
|
|
262
|
+
and a room without geometry is a scene without nodes and a file without the BIN chunk."""
|
|
263
|
+
scene = {"nodes": list(range(len(self.nodes)))} if self.nodes else {}
|
|
261
264
|
gltf = {
|
|
262
265
|
"asset": {"version": "2.0", "generator": "nnnotes/room", "extras": extras},
|
|
263
|
-
"scene": 0, "scenes": [
|
|
266
|
+
"scene": 0, "scenes": [scene],
|
|
264
267
|
"nodes": self.nodes, "meshes": self.meshes, "accessors": self.accessors,
|
|
265
|
-
"bufferViews": self.bufferViews, "buffers": [{"byteLength": len(self.bin)}],
|
|
268
|
+
"bufferViews": self.bufferViews, "buffers": [{"byteLength": len(self.bin)}] if self.bin else [],
|
|
266
269
|
"materials": self.materials, "textures": self.textures,
|
|
267
270
|
"images": self.images, "samplers": self.samplers,
|
|
268
271
|
}
|
|
272
|
+
gltf = {k: v for k, v in gltf.items() if v != []}
|
|
269
273
|
if self.ext_used:
|
|
270
274
|
gltf["extensionsUsed"] = sorted(self.ext_used)
|
|
271
275
|
js = dumps(gltf, separators=(",", ":")).encode("utf-8")
|
|
272
276
|
js += b" " * (-len(js) % 4)
|
|
273
277
|
self.bin.extend(b"\0" * (-len(self.bin) % 4))
|
|
278
|
+
size = 12 + 8 + len(js) + (8 + len(self.bin) if self.bin else 0)
|
|
274
279
|
with open(path, "wb") as f:
|
|
275
|
-
f.write(struct.pack("<III", 0x46546C67, 2,
|
|
280
|
+
f.write(struct.pack("<III", 0x46546C67, 2, size))
|
|
276
281
|
f.write(struct.pack("<II", len(js), 0x4E4F534A)); f.write(js)
|
|
277
|
-
|
|
282
|
+
if self.bin:
|
|
283
|
+
f.write(struct.pack("<II", len(self.bin), 0x004E4942)); f.write(self.bin)
|
|
278
284
|
|
|
279
285
|
|
|
280
286
|
# ---- extraction ---------------------------------------------------------
|
|
@@ -327,18 +327,23 @@ def _head_node(node: TypeTreeNode) -> TypeTreeNode | None:
|
|
|
327
327
|
return head
|
|
328
328
|
|
|
329
329
|
|
|
330
|
-
def script_class(obj) -> str:
|
|
331
|
-
"""Class name of a MonoBehaviour (needs the monoscript bundle loaded)
|
|
332
|
-
object's own typetree (not its fields); the whole object when that is
|
|
330
|
+
def script_class(obj) -> str | None:
|
|
331
|
+
"""Class name of a MonoBehaviour (needs the monoscript bundle loaded); None for a missing script (null m_Script).
|
|
332
|
+
Reads the header up to m_Script with the object's own typetree (not its fields); the whole object when that is
|
|
333
|
+
not possible."""
|
|
333
334
|
try:
|
|
334
335
|
head = _head_node(obj._get_typetree_node())
|
|
335
336
|
if head is not None:
|
|
336
|
-
|
|
337
|
-
if
|
|
338
|
-
return
|
|
337
|
+
pptr = obj.read_typetree(head, check_read=False)["m_Script"]
|
|
338
|
+
if not pptr["m_PathID"]:
|
|
339
|
+
return None
|
|
340
|
+
return deref(obj, pptr).read().m_ClassName
|
|
339
341
|
except Exception:
|
|
340
342
|
pass
|
|
341
|
-
|
|
343
|
+
script = obj.read().m_Script
|
|
344
|
+
if not script.m_PathID:
|
|
345
|
+
return None
|
|
346
|
+
return script.read().m_ClassName
|
|
342
347
|
|
|
343
348
|
|
|
344
349
|
def strip_pptrs(tt: dict) -> dict:
|
|
@@ -321,7 +321,13 @@ def source_entry(address: str, doc: dict, where: str) -> dict:
|
|
|
321
321
|
class ModelDir:
|
|
322
322
|
"""The models of `root`: each model in <root>/<id>/, the files of a model manifest (read_files; the shader index
|
|
323
323
|
reduced to the listed GLES3 programs as web.collect reduces it) in the layout export_model writes. ensure exports
|
|
324
|
-
a model whose directory does not exist, or every model once when `force`; `built` / `skipped`: the ids
|
|
324
|
+
a model whose directory does not exist, or every model once when `force`; `built` / `skipped`: the ids (a model
|
|
325
|
+
another process installed meanwhile is skipped).
|
|
326
|
+
|
|
327
|
+
Processes may share `root` (several `nnnotes story` at once): a model directory appears in one rename, whole, and
|
|
328
|
+
the first process to install a model wins; the others drop their export (the same bytes: exports are
|
|
329
|
+
deterministic) and use it. `force` moves a previous directory aside before installing; a process that finds the
|
|
330
|
+
directory gone in between exports the model itself."""
|
|
325
331
|
|
|
326
332
|
def __init__(self, cat, player, root: Path, force: bool = False):
|
|
327
333
|
self.cat, self.player, self.root, self.force = cat, player, Path(root), force
|
|
@@ -333,32 +339,42 @@ class ModelDir:
|
|
|
333
339
|
mid = model_id(address)
|
|
334
340
|
if mid not in self._done:
|
|
335
341
|
d = self.root / mid
|
|
336
|
-
if self.force or not d.exists():
|
|
337
|
-
self._export(address, d)
|
|
338
|
-
self.built.append(mid)
|
|
339
|
-
else:
|
|
340
|
-
self.skipped.append(mid)
|
|
341
342
|
index = d / MODEL_INDEX
|
|
342
|
-
|
|
343
|
+
built = (self.force or not d.exists()) and self._export(address, d)
|
|
344
|
+
if not index.is_file() and not d.exists(): # moved aside by a forced export of another process
|
|
345
|
+
built = self._export(address, d)
|
|
346
|
+
(self.built if built else self.skipped).append(mid)
|
|
347
|
+
try:
|
|
348
|
+
doc = json.loads(index.read_text(encoding="utf-8"))
|
|
349
|
+
except FileNotFoundError:
|
|
343
350
|
raise RuntimeError(f"{d}: no {MODEL_INDEX} (not a model directory); remove it or export again "
|
|
344
|
-
f"(--force)")
|
|
345
|
-
self._done[mid] = source_entry(address,
|
|
351
|
+
f"(--force)") from None
|
|
352
|
+
self._done[mid] = source_entry(address, doc, str(index))
|
|
346
353
|
return self._done[mid]
|
|
347
354
|
|
|
348
|
-
def _export(self, address: str, d: Path) ->
|
|
349
|
-
"""The model into a temporary directory
|
|
355
|
+
def _export(self, address: str, d: Path) -> bool:
|
|
356
|
+
"""The model into a temporary directory in `root`, then renamed to `d`; False when another process's `d` is
|
|
357
|
+
there instead (kept: without `force` any `d`, with `force` one installed after the previous `d` was moved
|
|
358
|
+
aside)."""
|
|
350
359
|
self.root.mkdir(parents=True, exist_ok=True)
|
|
351
360
|
work = Path(tempfile.mkdtemp(prefix=f".{d.name}-", dir=self.root))
|
|
352
361
|
try:
|
|
353
362
|
summary = export_model(self.cat, self.player, address, work / "export")
|
|
354
363
|
text, binary = collect(work / "export", summary["files"])
|
|
355
364
|
files = {**{p: t.encode("utf-8") for p, t in text.items()}, **binary}
|
|
365
|
+
model = work / "model"
|
|
356
366
|
for rel, data in sorted(files.items()):
|
|
357
|
-
(
|
|
358
|
-
(
|
|
359
|
-
if d
|
|
360
|
-
|
|
361
|
-
|
|
367
|
+
(model / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
368
|
+
(model / rel).write_bytes(data)
|
|
369
|
+
if _install(model, d):
|
|
370
|
+
return True
|
|
371
|
+
if not self.force:
|
|
372
|
+
return False
|
|
373
|
+
try:
|
|
374
|
+
d.rename(work / "previous")
|
|
375
|
+
except FileNotFoundError: # moved aside by another process
|
|
376
|
+
pass
|
|
377
|
+
return _install(model, d)
|
|
362
378
|
finally:
|
|
363
379
|
shutil.rmtree(work, ignore_errors=True)
|
|
364
380
|
|
|
@@ -367,6 +383,18 @@ class ModelDir:
|
|
|
367
383
|
return {"modelsDir": Path(os.path.relpath(os.path.abspath(self.root), os.path.abspath(story_dir))).as_posix()}
|
|
368
384
|
|
|
369
385
|
|
|
386
|
+
def _install(src: Path, d: Path) -> bool:
|
|
387
|
+
"""Rename the directory `src` to `d`; False when `d` exists (a non-empty directory: POSIX refuses to replace it,
|
|
388
|
+
Windows any directory)."""
|
|
389
|
+
try:
|
|
390
|
+
src.rename(d)
|
|
391
|
+
return True
|
|
392
|
+
except OSError:
|
|
393
|
+
if not d.exists():
|
|
394
|
+
raise
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
370
398
|
class SiteModels:
|
|
371
399
|
"""The models of the site at `site`: ensure reads model.json from the model manifest models/<id>.json (the models
|
|
372
400
|
are built before the stories: storysite.build)."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnnotes
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Offline data toolkit for BanG Dream! Our Notes game files: catalogs, bundles, master data, stories, Live2D, audio and live charts
|
|
5
5
|
Author: MetaMiku, emptysekai
|
|
6
6
|
License-Expression: MIT
|
|
@@ -105,13 +105,13 @@ Based on all data of the Taiwan server, version 1.0.1 (zh-Hant):
|
|
|
105
105
|
| catalog / bundle decryption / dependency closure | working |
|
|
106
106
|
| master data decoding | working |
|
|
107
107
|
| stories, `adv` | 946 / 946 episodes |
|
|
108
|
-
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list);
|
|
108
|
+
| stories, `story` (full directory) | 946 / 946 episodes have all their resources in the catalog and of supported kinds (the resource closure equals the game's own per-episode download list); 946 / 946 exported and checked one by one: the files, models and audio the output refers to exist, every JSON parses and every path it refers to resolves; the 235 episodes with frames, effects, post effects, stills, chats, talk windows or videos are byte-identical across two exports except for the models directory path |
|
|
109
109
|
| Live2D models | 239 / 239 (every model of the catalog; episodes use 185 of them) |
|
|
110
110
|
| CRI audio | 681 / 681 cue sheets |
|
|
111
111
|
| charts, `live` | 336 / 336 (music, difficulty) pairs |
|
|
112
|
-
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation;
|
|
113
|
-
| spots, `spot` / `room` |
|
|
114
|
-
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is
|
|
112
|
+
| web site, `web` | 336 / 336 charts, 239 / 239 Live2D models, 946 / 946 story episodes (two full builds in English without audio are byte-identical and 946 / 946 pass the data validation); two full builds of the three regions in five languages with AAC audio (`--all-regions`, with the live master data: 340 charts of 85 musics) are likewise byte-identical and pass the data validation in full |
|
|
113
|
+
| spots, `spot` / `room` | 39 / 39 spots, every reference resolves; `room` exports 20 / 20 room backgrounds of the catalog. Every glb passes the Khronos glTF-Validator with 0 errors |
|
|
114
|
+
| other regions (en / kr) and languages | checked: the regions serve the same catalog for a language and the same bundles, and the keys are shared; the master tables the charts use are the same in the three regions, and the text tables have all five languages; the chart exports checked match the Taiwan server's. A full multi-region site build is verified (the row above) |
|
|
115
115
|
|
|
116
116
|
## Requirements
|
|
117
117
|
|
|
@@ -332,6 +332,39 @@ def test_textures_with_the_same_png_bytes_share_one_image():
|
|
|
332
332
|
assert [im["name"] for im in glb.images] == ["a", "c"] and len(glb.bufferViews) == 2
|
|
333
333
|
|
|
334
334
|
|
|
335
|
+
def glb_chunks(path):
|
|
336
|
+
data = path.read_bytes()
|
|
337
|
+
magic, version, size = struct.unpack_from("<III", data, 0)
|
|
338
|
+
assert (magic, version, size) == (0x46546C67, 2, len(data))
|
|
339
|
+
chunks, at = [], 12
|
|
340
|
+
while at < len(data):
|
|
341
|
+
n, kind = struct.unpack_from("<II", data, at)
|
|
342
|
+
chunks.append((kind, data[at + 8:at + 8 + n]))
|
|
343
|
+
at += 8 + n
|
|
344
|
+
return chunks
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_a_room_without_geometry_is_a_valid_empty_gltf(tmp_path):
|
|
348
|
+
"""No empty arrays (glTF requires the arrays it has to be non-empty), no buffer and no BIN chunk."""
|
|
349
|
+
room._Glb().write(tmp_path / "room.glb", {"backgroundKey": "k"})
|
|
350
|
+
chunks = glb_chunks(tmp_path / "room.glb")
|
|
351
|
+
assert [kind for kind, _ in chunks] == [0x4E4F534A]
|
|
352
|
+
doc = json.loads(chunks[0][1])
|
|
353
|
+
assert doc == {"asset": {"version": "2.0", "generator": "nnnotes/room", "extras": {"backgroundKey": "k"}},
|
|
354
|
+
"scene": 0, "scenes": [{}]}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_a_room_with_geometry_keeps_its_arrays_and_bin_chunk(tmp_path):
|
|
358
|
+
glb = room._Glb()
|
|
359
|
+
glb.texture(b"png-1", "a", room._sampler({}))
|
|
360
|
+
glb.write(tmp_path / "room.glb", {})
|
|
361
|
+
chunks = glb_chunks(tmp_path / "room.glb")
|
|
362
|
+
assert [kind for kind, _ in chunks] == [0x4E4F534A, 0x004E4942]
|
|
363
|
+
doc = json.loads(chunks[0][1])
|
|
364
|
+
assert doc["buffers"] == [{"byteLength": 5}] and len(chunks[1][1]) == 8 # the chunk is padded to 4 bytes
|
|
365
|
+
assert list(doc) == ["asset", "scene", "scenes", "bufferViews", "buffers", "textures", "images", "samplers"]
|
|
366
|
+
|
|
367
|
+
|
|
335
368
|
def test_mesh_components_are_keyed_by_file_and_game_object():
|
|
336
369
|
bundle = SimpleNamespace(files={})
|
|
337
370
|
a, b = File("CAB-a", bundle), File("CAB-b", bundle)
|
|
@@ -137,6 +137,19 @@ def test_head_node_cut_after_script():
|
|
|
137
137
|
assert unity._head_node(TypeTreeNode(0, "X", "Base", -1, 1, m_Children=kids[:2])) is None
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def test_script_class_missing_script_is_none():
|
|
141
|
+
kids = [TypeTreeNode(1, t, n, 0, 0) for t, n in (("PPtr<GameObject>", "m_GameObject"), ("UInt8", "m_Enabled"),
|
|
142
|
+
("PPtr<MonoScript>", "m_Script"))]
|
|
143
|
+
node = TypeTreeNode(0, "MonoBehaviour", "Base", -1, 1, m_Children=kids)
|
|
144
|
+
null = {"m_FileID": 0, "m_PathID": 0}
|
|
145
|
+
obj = SimpleNamespace(_get_typetree_node=lambda: node,
|
|
146
|
+
read_typetree=lambda *a, **k: {"m_GameObject": null, "m_Enabled": 1, "m_Script": null})
|
|
147
|
+
assert unity.script_class(obj) is None
|
|
148
|
+
whole = SimpleNamespace(_get_typetree_node=lambda: None,
|
|
149
|
+
read=lambda: SimpleNamespace(m_Script=SimpleNamespace(m_PathID=0)))
|
|
150
|
+
assert unity.script_class(whole) is None
|
|
151
|
+
|
|
152
|
+
|
|
140
153
|
# ---------------------------------------------------------------- typetree values
|
|
141
154
|
def ref_value(ex, owner, v):
|
|
142
155
|
if unity.is_pptr(v):
|
|
@@ -642,6 +642,47 @@ def test_model_dir_exports_each_model_once(tmp_path, monkeypatch):
|
|
|
642
642
|
webmodel.ModelDir(None, player, root).ensure(KEY_B)
|
|
643
643
|
|
|
644
644
|
|
|
645
|
+
@pytest.mark.parametrize("force", [False, True])
|
|
646
|
+
def test_model_dir_shared_by_processes_keeps_one_whole_model(tmp_path, monkeypatch, force):
|
|
647
|
+
"""Another process (another ModelDir on the same root) installs the model while this one exports it: without
|
|
648
|
+
force the first installed copy is kept and this export dropped, with force this export replaces it; either way
|
|
649
|
+
one whole model directory and no temporary directory is left."""
|
|
650
|
+
player = fake_export(monkeypatch, motion_sync=True)
|
|
651
|
+
root, real, other = tmp_path / "live2d", webmodel.export_model, []
|
|
652
|
+
|
|
653
|
+
def racing(*a):
|
|
654
|
+
if not other:
|
|
655
|
+
other.append(webmodel.ModelDir(None, player, root, force=force))
|
|
656
|
+
other[0].ensure(KEY_A)
|
|
657
|
+
return real(*a)
|
|
658
|
+
monkeypatch.setattr(webmodel, "export_model", racing)
|
|
659
|
+
a = webmodel.ModelDir(None, player, root, force=force)
|
|
660
|
+
assert a.ensure(KEY_A) == {"id": "adv_model_a", "motionSync": True}
|
|
661
|
+
assert (a.built, a.skipped) == ((["adv_model_a"], []) if force else ([], ["adv_model_a"]))
|
|
662
|
+
assert other[0].built == ["adv_model_a"]
|
|
663
|
+
assert [p.name for p in root.iterdir()] == ["adv_model_a"]
|
|
664
|
+
assert dir_files(root) == [f"adv_model_a/{f}" for f in model_files("adv_model_a")]
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def test_model_dir_exports_a_model_moved_aside_after_the_check(tmp_path, monkeypatch):
|
|
668
|
+
"""The model directory existed when checked but is gone when read (a forced export of another process moved it
|
|
669
|
+
aside): exported here."""
|
|
670
|
+
player = fake_export(monkeypatch, motion_sync=True)
|
|
671
|
+
root = tmp_path / "live2d"
|
|
672
|
+
d, real, seen = root / "adv_model_a", webmodel.Path.exists, []
|
|
673
|
+
|
|
674
|
+
def exists(p):
|
|
675
|
+
if p == d and not seen:
|
|
676
|
+
seen.append(p)
|
|
677
|
+
return True
|
|
678
|
+
return real(p)
|
|
679
|
+
monkeypatch.setattr(webmodel.Path, "exists", exists)
|
|
680
|
+
a = webmodel.ModelDir(None, player, root)
|
|
681
|
+
assert a.ensure(KEY_A) == {"id": "adv_model_a", "motionSync": True}
|
|
682
|
+
assert seen and (a.built, a.skipped) == (["adv_model_a"], [])
|
|
683
|
+
assert dir_files(root) == [f"adv_model_a/{f}" for f in model_files("adv_model_a")]
|
|
684
|
+
|
|
685
|
+
|
|
645
686
|
def test_story_command_exports_its_models_next_to_the_story(tmp_path, capsys, monkeypatch):
|
|
646
687
|
from nnnotes import story
|
|
647
688
|
player = fake_export(monkeypatch)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|