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.
Files changed (145) hide show
  1. {nnnotes-0.1.1/src/nnnotes.egg-info → nnnotes-0.1.2}/PKG-INFO +5 -5
  2. {nnnotes-0.1.1 → nnnotes-0.1.2}/README.en.md +4 -4
  3. {nnnotes-0.1.1 → nnnotes-0.1.2}/README.md +4 -4
  4. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/__init__.py +1 -1
  5. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/room.py +10 -4
  6. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/unity.py +12 -7
  7. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/webmodel.py +44 -16
  8. {nnnotes-0.1.1 → nnnotes-0.1.2/src/nnnotes.egg-info}/PKG-INFO +5 -5
  9. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_room.py +33 -0
  10. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_speed_exact.py +13 -0
  11. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_webmodel.py +41 -0
  12. {nnnotes-0.1.1 → nnnotes-0.1.2}/LICENSE +0 -0
  13. {nnnotes-0.1.1 → nnnotes-0.1.2}/pyproject.toml +0 -0
  14. {nnnotes-0.1.1 → nnnotes-0.1.2}/setup.cfg +0 -0
  15. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/__main__.py +0 -0
  16. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/acb.py +0 -0
  17. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/addressables.py +0 -0
  18. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/adv.py +0 -0
  19. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advcommand.py +0 -0
  20. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advmedia.py +0 -0
  21. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advscene.py +0 -0
  22. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advui.py +0 -0
  23. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/advvideo.py +0 -0
  24. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/__init__.py +0 -0
  25. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/astc.py +0 -0
  26. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/gltf.py +0 -0
  27. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/mesh.py +0 -0
  28. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/png.py +0 -0
  29. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/reader.py +0 -0
  30. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/sniff.py +0 -0
  31. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/sprite.py +0 -0
  32. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/atoms/texture.py +0 -0
  33. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cache.py +0 -0
  34. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/catalog.py +0 -0
  35. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/catalogdb.py +0 -0
  36. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/census.py +0 -0
  37. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cli.py +0 -0
  38. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cli_assets.py +0 -0
  39. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/compress.py +0 -0
  40. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/config.py +0 -0
  41. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/configfile.py +0 -0
  42. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/contract.py +0 -0
  43. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cri.py +0 -0
  44. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/crikey.py +0 -0
  45. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/crilips.py +0 -0
  46. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/cristages.py +0 -0
  47. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/deckdata.py +0 -0
  48. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/export.py +0 -0
  49. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/gameapi.py +0 -0
  50. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/jsonio.py +0 -0
  51. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/languages.py +0 -0
  52. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/layout.py +0 -0
  53. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/link.py +0 -0
  54. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/live.py +0 -0
  55. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/live2d.py +0 -0
  56. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveaudio.py +0 -0
  57. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/livenotes.py +0 -0
  58. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveoptions.py +0 -0
  59. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/livescene.py +0 -0
  60. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/liveui.py +0 -0
  61. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/master.py +0 -0
  62. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/modelstages.py +0 -0
  63. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/motion.py +0 -0
  64. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/nnnotes.example.toml +0 -0
  65. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/objexport.py +0 -0
  66. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/orchestrate.py +0 -0
  67. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/plan.py +0 -0
  68. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/player.py +0 -0
  69. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/score.py +0 -0
  70. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/shader.py +0 -0
  71. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/spot.py +0 -0
  72. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/stages.py +0 -0
  73. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/store.py +0 -0
  74. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/story.py +0 -0
  75. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyfonts.py +0 -0
  76. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyhost.py +0 -0
  77. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storysite.py +0 -0
  78. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/storyvoices.py +0 -0
  79. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/textstyle.py +0 -0
  80. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/tmpfont.py +0 -0
  81. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/typetrees/6000.3.12f1.json +0 -0
  82. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/viewrules.json +0 -0
  83. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/views.py +0 -0
  84. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/voices.py +0 -0
  85. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/web.py +0 -0
  86. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes/webaudio.py +0 -0
  87. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/SOURCES.txt +0 -0
  88. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/dependency_links.txt +0 -0
  89. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/entry_points.txt +0 -0
  90. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/requires.txt +0 -0
  91. {nnnotes-0.1.1 → nnnotes-0.1.2}/src/nnnotes.egg-info/top_level.txt +0 -0
  92. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_advvideo.py +0 -0
  93. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atom_eval.py +0 -0
  94. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_mesh.py +0 -0
  95. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_sprite.py +0 -0
  96. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_atoms_texture.py +0 -0
  97. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_blob_memo.py +0 -0
  98. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cache.py +0 -0
  99. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalog.py +0 -0
  100. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalog_lazy.py +0 -0
  101. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_catalogdb.py +0 -0
  102. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_census.py +0 -0
  103. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cli.py +0 -0
  104. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cli_assets.py +0 -0
  105. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_compress.py +0 -0
  106. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_config.py +0 -0
  107. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_configfile.py +0 -0
  108. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_contract.py +0 -0
  109. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_crilips.py +0 -0
  110. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_cristages.py +0 -0
  111. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_deckdata.py +0 -0
  112. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_gameapi.py +0 -0
  113. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_goldens.py +0 -0
  114. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_jsonio.py +0 -0
  115. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_layout.py +0 -0
  116. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_link.py +0 -0
  117. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_live_options.py +0 -0
  118. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_live_options_web.py +0 -0
  119. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_master.py +0 -0
  120. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_mesh_texture_guards.py +0 -0
  121. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_modelstages.py +0 -0
  122. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_objexport.py +0 -0
  123. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_orchestrate.py +0 -0
  124. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_pipeline.py +0 -0
  125. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_plan.py +0 -0
  126. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_player.py +0 -0
  127. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_readsets.py +0 -0
  128. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_regions.py +0 -0
  129. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_repo.py +0 -0
  130. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_sniff.py +0 -0
  131. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_spot.py +0 -0
  132. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_stages.py +0 -0
  133. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_store.py +0 -0
  134. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_closure.py +0 -0
  135. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_fonts.py +0 -0
  136. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_host.py +0 -0
  137. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_kinds.py +0 -0
  138. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_site.py +0 -0
  139. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_story_ui.py +0 -0
  140. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_textstyle.py +0 -0
  141. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_typetrees.py +0 -0
  142. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_views.py +0 -0
  143. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_voices.py +0 -0
  144. {nnnotes-0.1.1 → nnnotes-0.1.2}/tests/test_voices_story.py +0 -0
  145. {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.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); 759 exported one by one, the other 187 (with frames, effects, post effects, stills and the like) not yet one by one |
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; a 27-episode sample in five languages with AAC audio is likewise identical across two builds and valid) |
113
- | spots, `spot` / `room` | one spot verified, the others not individually checked |
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 not verified yet |
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); 759 exported one by one, the other 187 (with frames, effects, post effects, stills and the like) not yet one by one |
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; a 27-episode sample in five languages with AAC audio is likewise identical across two builds and valid) |
82
- | spots, `spot` / `room` | one spot verified, the others not individually checked |
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 not verified yet |
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 中且类型均受支持(资源闭包与游戏自带的每集下载清单一致);已逐集导出验证 759 集,其余 187 集(用到 Frame / Effect / PostEffect / Still 等资源)未逐一验证 |
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 集通过数据校验;五种语言含 AAC 音频的 27 集抽样同样一致,并通过数据校验) |
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.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": [{"nodes": list(range(len(self.nodes)))}],
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, 12 + 8 + len(js) + 8 + len(self.bin)))
280
+ f.write(struct.pack("<III", 0x46546C67, 2, size))
276
281
  f.write(struct.pack("<II", len(js), 0x4E4F534A)); f.write(js)
277
- f.write(struct.pack("<II", len(self.bin), 0x004E4942)); f.write(self.bin)
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). Reads the header up to m_Script with the
332
- object's own typetree (not its fields); the whole object when that is not possible."""
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
- ms = deref(obj, obj.read_typetree(head, check_read=False)["m_Script"])
337
- if ms is not None:
338
- return ms.read().m_ClassName
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
- return obj.read().m_Script.read().m_ClassName
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
- if not index.is_file():
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, json.loads(index.read_text(encoding="utf-8")), str(index))
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) -> None:
349
- """The model into a temporary directory next to `d`, then renamed to `d` (a previous `d` removed)."""
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
- (work / "model" / rel).parent.mkdir(parents=True, exist_ok=True)
358
- (work / "model" / rel).write_bytes(data)
359
- if d.exists():
360
- shutil.rmtree(d)
361
- (work / "model").rename(d)
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.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); 759 exported one by one, the other 187 (with frames, effects, post effects, stills and the like) not yet one by one |
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; a 27-episode sample in five languages with AAC audio is likewise identical across two builds and valid) |
113
- | spots, `spot` / `room` | one spot verified, the others not individually checked |
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 not verified yet |
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