nnnotes 0.1.3__tar.gz → 0.1.4__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 (93) hide show
  1. nnnotes-0.1.4/CHANGELOG.md +161 -0
  2. {nnnotes-0.1.3 → nnnotes-0.1.4}/PKG-INFO +12 -1
  3. {nnnotes-0.1.3 → nnnotes-0.1.4}/README.en.md +11 -0
  4. {nnnotes-0.1.3 → nnnotes-0.1.4}/pyproject.toml +3 -2
  5. {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/Cargo.lock +75 -5
  6. nnnotes-0.1.4/rust/Cargo.toml +24 -0
  7. {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/build.rs +6 -6
  8. {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/src/lib.rs +8 -8
  9. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/__init__.py +1 -1
  10. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cli.py +8 -3
  11. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cli_assets.py +30 -4
  12. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/deckdata.py +3 -2
  13. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/musicdata.py +5 -3
  14. nnnotes-0.1.4/src/nnnotes/replaydata.py +138 -0
  15. nnnotes-0.1.3/rust/Cargo.toml +0 -23
  16. nnnotes-0.1.3/src/nnnotes/replaydata.py +0 -101
  17. {nnnotes-0.1.3 → nnnotes-0.1.4}/LICENSE +0 -0
  18. {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/rustfmt.toml +0 -0
  19. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/__main__.py +0 -0
  20. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/acb.py +0 -0
  21. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/addressables.py +0 -0
  22. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/adv.py +0 -0
  23. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advcommand.py +0 -0
  24. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advmedia.py +0 -0
  25. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advscene.py +0 -0
  26. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advui.py +0 -0
  27. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advvideo.py +0 -0
  28. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/apkset.py +0 -0
  29. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/__init__.py +0 -0
  30. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/astc.py +0 -0
  31. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/gltf.py +0 -0
  32. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/mesh.py +0 -0
  33. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/png.py +0 -0
  34. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/reader.py +0 -0
  35. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/sniff.py +0 -0
  36. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/sprite.py +0 -0
  37. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/texture.py +0 -0
  38. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cache.py +0 -0
  39. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/catalog.py +0 -0
  40. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/catalogdb.py +0 -0
  41. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/census.py +0 -0
  42. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/compress.py +0 -0
  43. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/config.py +0 -0
  44. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/configfile.py +0 -0
  45. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/contract.py +0 -0
  46. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cri.py +0 -0
  47. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/crikey.py +0 -0
  48. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/crilips.py +0 -0
  49. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cristages.py +0 -0
  50. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/export.py +0 -0
  51. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/gameapi.py +0 -0
  52. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/jp.py +0 -0
  53. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/jsonio.py +0 -0
  54. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/languages.py +0 -0
  55. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/layout.py +0 -0
  56. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/link.py +0 -0
  57. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/live.py +0 -0
  58. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/live2d.py +0 -0
  59. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveaudio.py +0 -0
  60. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/livenotes.py +0 -0
  61. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveoptions.py +0 -0
  62. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/livescene.py +0 -0
  63. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveui.py +0 -0
  64. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/master.py +0 -0
  65. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/modelstages.py +0 -0
  66. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/motion.py +0 -0
  67. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/nnnotes.example.toml +0 -0
  68. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/objexport.py +0 -0
  69. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/orchestrate.py +0 -0
  70. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/plan.py +0 -0
  71. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/player.py +0 -0
  72. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/room.py +0 -0
  73. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/score.py +0 -0
  74. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/shader.py +0 -0
  75. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/spot.py +0 -0
  76. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/stages.py +0 -0
  77. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/store.py +0 -0
  78. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/story.py +0 -0
  79. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyfonts.py +0 -0
  80. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyhost.py +0 -0
  81. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storysite.py +0 -0
  82. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyvoices.py +0 -0
  83. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/textstyle.py +0 -0
  84. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/tmpfont.py +0 -0
  85. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/typetrees/6000.3.12f1.json +0 -0
  86. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/ui.py +0 -0
  87. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/unity.py +0 -0
  88. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/viewrules.json +0 -0
  89. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/views.py +0 -0
  90. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/voices.py +0 -0
  91. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/web.py +0 -0
  92. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/webaudio.py +0 -0
  93. {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/webmodel.py +0 -0
@@ -0,0 +1,161 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are listed here. Versions follow [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [0.1.4](https://github.com/MetaSekaiLab/nnnotes/compare/v0.1.3...v0.1.4) - 2026-10-06
6
+
7
+ ### Features
8
+
9
+ - **deck-data:** Export character rank experience thresholds ([3abe101](https://github.com/MetaSekaiLab/nnnotes/commit/3abe1017e47d267f77822af949500789c1831ee4))
10
+ - **music-data:** Publish the recommendation engine in the replay manifest ([42098d7](https://github.com/MetaSekaiLab/nnnotes/commit/42098d7bfa538912bbc350beaebfe78f3b04f202))
11
+ - **catalogs:** Record the resource version of an imported catalog ([a9d0bbd](https://github.com/MetaSekaiLab/nnnotes/commit/a9d0bbd034aee00c696979eba3b1f33754b8a237))
12
+ - **music-data:** Take the deck model's WASM release packages as engines ([de34ee2](https://github.com/MetaSekaiLab/nnnotes/commit/de34ee28ec181cac16237aa2751ff38e3bccfd91))
13
+
14
+ ### Bug fixes
15
+
16
+ - **deck-data:** Export represented VIP rank identities ([c5d0514](https://github.com/MetaSekaiLab/nnnotes/commit/c5d051454f90c2afc7fb0063485f7dcf5d5a7669))
17
+ - **music-data:** Name the mismatch when an engine package is rejected ([e7fdacd](https://github.com/MetaSekaiLab/nnnotes/commit/e7fdacdd34b80fdf391614f08fd5d7480d33904e))
18
+
19
+ ### Documentation
20
+
21
+ - Add a changelog generated by git-cliff ([d601820](https://github.com/MetaSekaiLab/nnnotes/commit/d60182018911d3f1fc236d355185592b2b6ec8ad))
22
+
23
+ ### Build
24
+
25
+ - **deck:** Build the deck model from the ournotes-sim crate ([946fb33](https://github.com/MetaSekaiLab/nnnotes/commit/946fb335aef234837c461dfa960f3ce28dc946da))
26
+ - **deck:** Ournotes-deck ddf212b601731f9ab74245119d4408d2746a370c ([9f0ba0e](https://github.com/MetaSekaiLab/nnnotes/commit/9f0ba0e234cc4119682b3be4ebcd85344e618b02))
27
+ - **deck:** Ournotes-deck v0.0.2 ([8a088f4](https://github.com/MetaSekaiLab/nnnotes/commit/8a088f4b7e40a79b3bc111e88c25d0abce397f36))
28
+
29
+ ### CI
30
+
31
+ - **commitlint:** Lift the line length limit of bodies and footers ([b9eecd9](https://github.com/MetaSekaiLab/nnnotes/commit/b9eecd91c682011748a3877e311d6ddcfb88000b))
32
+ - Test on Python 3.14 ([5c845eb](https://github.com/MetaSekaiLab/nnnotes/commit/5c845eb365fcc23c36bf724f336ed747ba198057))
33
+ - **deck:** Move create-pull-request to v8, which runs on Node.js 24 ([1e1bb17](https://github.com/MetaSekaiLab/nnnotes/commit/1e1bb179f98dddecb72465949276cbc4fb4e8fd7))
34
+ - **deck:** Dispatch CI and the commit message check on the pin pull request ([3a6bdfc](https://github.com/MetaSekaiLab/nnnotes/commit/3a6bdfc95dd80171724ac313e2aeb5278859a843))
35
+
36
+ ## [0.1.3](https://github.com/MetaSekaiLab/nnnotes/compare/v0.1.2...v0.1.3) - 2026-09-30
37
+
38
+ ### Features
39
+
40
+ - **songs:** Export every song's metadata and chart facts ([3dae671](https://github.com/MetaSekaiLab/nnnotes/commit/3dae671302ca843c2ce573802e1309b4b85893a2))
41
+ - [**breaking**] One music-data command with the deck model built in ([197b436](https://github.com/MetaSekaiLab/nnnotes/commit/197b436dfccbefa5296fd40fcf5167458ab1406e))
42
+ - **music-data:** Play scenarios: Gekisou off, every rank, the Perfect play ([a03591e](https://github.com/MetaSekaiLab/nnnotes/commit/a03591e7fd4f186df7ddbe161c787c7cad138b41))
43
+ - **music-data:** Read decoded master data with its manifest (--decoded-master) ([12df2a6](https://github.com/MetaSekaiLab/nnnotes/commit/12df2a67b71ade5e41b51e6602d97d6b5733bd99))
44
+ - **jp:** Support Japanese release assets and split APKs ([9bd4452](https://github.com/MetaSekaiLab/nnnotes/commit/9bd4452b262b349cf4ae7b20b4110f532f2ba8d4))
45
+ - **music-data:** The Gekisou catalog, range luck points and the seed set check ([ffaa1bc](https://github.com/MetaSekaiLab/nnnotes/commit/ffaa1bc10eecc2269ffcffb5fa952e166dc1053f))
46
+ - **music-data:** Export and validate single-skill Gekisou aptitude ([35ae7a0](https://github.com/MetaSekaiLab/nnnotes/commit/35ae7a0727934f9f12a8a0b1b650b3a42c6f9a5f))
47
+ - Export versioned Rust replay data with chart statistics ([46b5b09](https://github.com/MetaSekaiLab/nnnotes/commit/46b5b09c7356c27874ee01c610dc295f60b48765))
48
+ - **ui:** Export offline serialized prefab libraries ([0a7344d](https://github.com/MetaSekaiLab/nnnotes/commit/0a7344d09dbeddde36237b8bf63e83bee2159885))
49
+
50
+ ### Bug fixes
51
+
52
+ - **jp:** Address review regressions ([5f40d3e](https://github.com/MetaSekaiLab/nnnotes/commit/5f40d3ee2b195b802a97bf4a861e3f64d223d1f6))
53
+ - **catalog:** Select international catalogs by resource version ([15cb3bf](https://github.com/MetaSekaiLab/nnnotes/commit/15cb3bfe079282f2624c5a0eeba757566c7eb5f6))
54
+ - **release:** Preflight wheels and preserve reviewed notes ([b1e12c4](https://github.com/MetaSekaiLab/nnnotes/commit/b1e12c4ab31cfed6cd453e562aeeb66c3821af83))
55
+
56
+ ### Build
57
+
58
+ - **deck:** Pin the final Gekisou aptitude implementation ([4881a21](https://github.com/MetaSekaiLab/nnnotes/commit/4881a2183a1d1cef9ecc799f0bf293dbc58469d7))
59
+
60
+ ### CI
61
+
62
+ - Move the GitHub Actions to their current majors ([f0038c1](https://github.com/MetaSekaiLab/nnnotes/commit/f0038c15887b2ad8b61ddde320560837dbab1b5f))
63
+ - Run commitlint on Node.js 24, the active LTS ([48b6557](https://github.com/MetaSekaiLab/nnnotes/commit/48b6557490f638f2b37b73dabb8fa175f0cce788))
64
+
65
+ ## [0.1.2](https://github.com/MetaSekaiLab/nnnotes/compare/v0.1.1...v0.1.2) - 2026-09-28
66
+
67
+ ### Bug fixes
68
+
69
+ - **unity:** Treat a MonoBehaviour with a missing script as classless ([83dd161](https://github.com/MetaSekaiLab/nnnotes/commit/83dd161e7a2af6953e97c56548341ab6839def0c))
70
+ - **room:** Write a valid glTF for a room without geometry ([8465dd5](https://github.com/MetaSekaiLab/nnnotes/commit/8465dd5d89d3a1857d4dcb80b9f5ff8a36895473))
71
+ - **story:** Share a models directory between concurrent story runs ([2051001](https://github.com/MetaSekaiLab/nnnotes/commit/2051001924ecdbb39d6bc6c5d011a9b52d43a188))
72
+
73
+ ### Documentation
74
+
75
+ - **readme:** Completion after verifying every story, spot and region ([c4d6740](https://github.com/MetaSekaiLab/nnnotes/commit/c4d67403b434a05f011a247a5c5bf48f52ebb9d1))
76
+
77
+ ## [0.1.1](https://github.com/MetaSekaiLab/nnnotes/compare/v0.1.0...v0.1.1) - 2026-09-27
78
+
79
+ ### Features
80
+
81
+ - **config:** Write, edit and check the config file ([1f9b1a2](https://github.com/MetaSekaiLab/nnnotes/commit/1f9b1a2ab9ecb1a29a328a88c8dbf351e010597e))
82
+ - **web:** [**breaking**] Store site assets compressed, gzip by default ([e90a46b](https://github.com/MetaSekaiLab/nnnotes/commit/e90a46b19f332e137b0fa49316651e2fb014a464))
83
+ - **story:** [**breaking**] Stories reference their Live2D models instead of embedding them ([510ff98](https://github.com/MetaSekaiLab/nnnotes/commit/510ff98d90d19bee91e6f87f555c5fbb4104e08b))
84
+
85
+ ### Bug fixes
86
+
87
+ - **room:** Share equal images and key objects by their file ([a8af00a](https://github.com/MetaSekaiLab/nnnotes/commit/a8af00a5f89afa11f0cdb0caf032afe38e6e1b18))
88
+
89
+ ## [0.1.0](https://github.com/MetaSekaiLab/nnnotes/releases/tag/v0.1.0) - 2026-09-27
90
+
91
+ ### Features
92
+
93
+ - Add the nnnotes data toolkit ([073c60c](https://github.com/MetaSekaiLab/nnnotes/commit/073c60cc029b067e2b5db9a8c5772f7dbdcc58d4))
94
+ - Read the APK boot data with bundled type trees ([fb1739e](https://github.com/MetaSekaiLab/nnnotes/commit/fb1739ebe5b898dcf5c6a4c02b4eb88f6f59ec28))
95
+ - Build Live2D models into the web site ([03e3831](https://github.com/MetaSekaiLab/nnnotes/commit/03e38314eebf7d6a9bb62b5905778056b73d33c3))
96
+ - Fetch the current master data version from the game's API ([bcd7f20](https://github.com/MetaSekaiLab/nnnotes/commit/bcd7f20d9bb1e3bfeb89dd72f49f5d360b8da2ae))
97
+ - Serve every region and language from one web site ([8160121](https://github.com/MetaSekaiLab/nnnotes/commit/81601210452e3b40f195e115beaae0dccf10f7cc))
98
+ - Add open and game text styles for exported text (--fonts) ([da41edf](https://github.com/MetaSekaiLab/nnnotes/commit/da41edf7fe17fff125f56d9a5d5ab2afa5e3f8f6))
99
+ - Export frames, effects, stills, chat and videos of story episodes ([2bacea1](https://github.com/MetaSekaiLab/nnnotes/commit/2bacea1e02054393b4d8bc09d7325c3fa2be06da))
100
+ - Add the asset export: export, plan, run-stage, catalogs ([d04423c](https://github.com/MetaSekaiLab/nnnotes/commit/d04423c2db72eb8d350c8271aaf2c9ea4bf64b88))
101
+ - **live:** Offer the Live options that select files ([dc4f1e7](https://github.com/MetaSekaiLab/nnnotes/commit/dc4f1e704ccdebba878f179f24aa2157d6186c88))
102
+ - **web:** Export story episodes for the story player ([197af04](https://github.com/MetaSekaiLab/nnnotes/commit/197af04954c4f5bd4dc6932140f57c0b7f937452))
103
+ - **export:** FLAC level 8 by default and explicit layout placement ([11bf5d8](https://github.com/MetaSekaiLab/nnnotes/commit/11bf5d87f6decd8b0d465661c8d41f48b227945e))
104
+ - **story:** Chat texts, emoji sprites, frame particles, lit rooms and CRI Lips data ([5eb4f6c](https://github.com/MetaSekaiLab/nnnotes/commit/5eb4f6cfa1978eab6846ff6cc99b48a997b90efc))
105
+ - **deck-data:** One JSON of charts and master tables for deck-building tools ([06e6943](https://github.com/MetaSekaiLab/nnnotes/commit/06e6943e4a2ad977d6684871906f7d90a282a227))
106
+ - **voices:** Voice index view and query command; one ACB table reader ([6bb748a](https://github.com/MetaSekaiLab/nnnotes/commit/6bb748aaa7e24942f5cee6ebb704e7c016e9a8c7))
107
+ - **cri.movie:** All USM streams (MPEG-1, H.264, alpha, subtitles, HCA) and bundle-held movies ([ff97da8](https://github.com/MetaSekaiLab/nnnotes/commit/ff97da8b2cb01721041522f69d8c42c978b5150c))
108
+ - **voices:** Story episode voices with their lines, speakers and cues ([e65d84d](https://github.com/MetaSekaiLab/nnnotes/commit/e65d84d9733e039bf046c3bfd56a5cda06eba118))
109
+ - **story:** Missing glyphs and frame texts in the story font data ([476dff0](https://github.com/MetaSekaiLab/nnnotes/commit/476dff0c02f47017f26260997fb2b8bce0d2078e))
110
+ - **story:** Open font assets follow the game's TMP fallback chains ([b5cd68e](https://github.com/MetaSekaiLab/nnnotes/commit/b5cd68ee7bbd3dc2dc58c9fb8858a20b34e7179c))
111
+
112
+ ### Bug fixes
113
+
114
+ - Correct the ADV resource closure for chat stamps and IgnoreData rows ([9f03fb9](https://github.com/MetaSekaiLab/nnnotes/commit/9f03fb97784ecdfbeb0e576c8e257c0e6123b05c))
115
+ - Stop with a usage error on unknown keys, ids and charts ([1fa6958](https://github.com/MetaSekaiLab/nnnotes/commit/1fa69589a68246ffa2cbd5a96bda8c1646be37b1))
116
+ - Read the region, CDN and bundle key only when a download needs them ([da1d47c](https://github.com/MetaSekaiLab/nnnotes/commit/da1d47c918d7895e2f83390cf69f888817d0fcb9))
117
+ - Handle more mesh, texture and reference forms in room and spot ([4ddc76c](https://github.com/MetaSekaiLab/nnnotes/commit/4ddc76ca667d5458a77928560e562060a331cf6b))
118
+ - Generate MonoBehaviour typetrees in Unity's own node layout ([d0efb2c](https://github.com/MetaSekaiLab/nnnotes/commit/d0efb2cbbe17a2749fdb7f8f6b2370d25de95917))
119
+ - Story UI language, model names, master JSON and external AWB ([29aada6](https://github.com/MetaSekaiLab/nnnotes/commit/29aada69e763dde8c2fd51fe287dfea118314766))
120
+ - **export:** Read the APK's raw files from the APK ([3d9f79c](https://github.com/MetaSekaiLab/nnnotes/commit/3d9f79c135afb232a12ed3661584f780187a9b4b))
121
+ - Size default worker counts by the CPUs the process may use ([a65eaf8](https://github.com/MetaSekaiLab/nnnotes/commit/a65eaf89d9726dcd3200166c3483cded94b30633))
122
+ - **web:** Name --story when the fonts extra is missing ([8cb0f66](https://github.com/MetaSekaiLab/nnnotes/commit/8cb0f66f8e2ffcbc0171bbd0fc1bfbac9be09baa))
123
+
124
+ ### Performance
125
+
126
+ - Reuse decoded textures, shader dumps and cue sheets by content ([1f7c166](https://github.com/MetaSekaiLab/nnnotes/commit/1f7c166d008eb06b0ac6f193ad7958fb88dbf7b4))
127
+ - Pipeline the web build and reuse scenes, BGM decodes and read sets ([25461d1](https://github.com/MetaSekaiLab/nnnotes/commit/25461d12c715369285027d008ebc53f49c73acb6))
128
+ - Start numpy with one OpenBLAS thread unless the environment sets it ([f93d3d2](https://github.com/MetaSekaiLab/nnnotes/commit/f93d3d2d4e1f34e863d96cf17175ead1376d09cc))
129
+ - Serve read-set plans from long-lived player processes ([440ceaa](https://github.com/MetaSekaiLab/nnnotes/commit/440ceaad5c63e83bba4b3a2dde2592467b61c111))
130
+ - **export:** Bound the memory of the finalize and of plan -o ([cad57e6](https://github.com/MetaSekaiLab/nnnotes/commit/cad57e61c4483300f402418501bfb754558287bf))
131
+ - **cli:** Start without UnityPy; --flac-level on audio, live and story ([c5057d5](https://github.com/MetaSekaiLab/nnnotes/commit/c5057d5855b3703308c9b5d7d762821a0498bfaa))
132
+
133
+ ### Refactoring
134
+
135
+ - Rename the site command to web ([43fe4cc](https://github.com/MetaSekaiLab/nnnotes/commit/43fe4cc3e016aa8a773182f8e8995b1ef27b1066))
136
+
137
+ ### Documentation
138
+
139
+ - Remove the contributing guide ([056b4f6](https://github.com/MetaSekaiLab/nnnotes/commit/056b4f64316a5949f99f1cbefd3f0f1d7692a97e))
140
+ - Title the project nnnotes?! ([91b5042](https://github.com/MetaSekaiLab/nnnotes/commit/91b50422fc389945367924bcfdf773954fdafd03))
141
+ - State determinism with its condition; settings per command ([9b0c6f7](https://github.com/MetaSekaiLab/nnnotes/commit/9b0c6f79b651303fca979b3811ffe62d9b7c0c00))
142
+ - Recommend Python 3.13 ([cfea0f5](https://github.com/MetaSekaiLab/nnnotes/commit/cfea0f56be5758dc8b7fa3ca0f8f0191fc38336a))
143
+ - Add performance.md: where a web build spends its time ([a4fb6a0](https://github.com/MetaSekaiLab/nnnotes/commit/a4fb6a0db5a6976630ac9cb069e026078772823a))
144
+ - **performance:** Incremental runs and raw files not in the cache ([69a879c](https://github.com/MetaSekaiLab/nnnotes/commit/69a879ca1d64aecdd5eea3e352cd0861f0a88a66))
145
+
146
+ ### Build
147
+
148
+ - **release:** Publish to PyPI with trusted publishing ([bb542af](https://github.com/MetaSekaiLab/nnnotes/commit/bb542af0d7e22a0255a7ee45099a51eb7fbadd12))
149
+
150
+ ### CI
151
+
152
+ - Check the stage golden records with pinned library versions ([f2594f7](https://github.com/MetaSekaiLab/nnnotes/commit/f2594f70366c8b46d70d638168d43f6dd7949e5b))
153
+
154
+ ### Miscellaneous
155
+
156
+ - List MetaMiku and emptysekai as copyright holders ([1629616](https://github.com/MetaSekaiLab/nnnotes/commit/1629616da629b7fbe7f714287aeff17933176357))
157
+ - List MetaMiku as an author ([cc43e13](https://github.com/MetaSekaiLab/nnnotes/commit/cc43e132875206d02d8665b19c06b27255ab0037))
158
+ - Point the project links at MetaSekaiLab/nnnotes ([a2b8304](https://github.com/MetaSekaiLab/nnnotes/commit/a2b83040bcf06771028bbfd5e90c10c225bde3e9))
159
+ - English package readme; check commits on pull requests only ([e602dd5](https://github.com/MetaSekaiLab/nnnotes/commit/e602dd5ebf6dda0f18da5ace54a02dbb893a796f))
160
+
161
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnnotes
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Operating System :: OS Independent
6
6
  Classifier: Topic :: Multimedia
@@ -221,6 +221,17 @@ everywhere.
221
221
  See [docs/performance.md](https://github.com/MetaSekaiLab/nnnotes/blob/main/docs/performance.md) for where a
222
222
  `web` build spends its time and which parts of it run in compiled code.
223
223
 
224
+ ## Changelog
225
+
226
+ [CHANGELOG.md](https://github.com/MetaSekaiLab/nnnotes/blob/main/CHANGELOG.md) lists the changes in every version,
227
+ generated from the commits by [git-cliff](https://git-cliff.org/). The commit that bumps the version regenerates it:
228
+
229
+ ```bash
230
+ git cliff --tag vX.Y.Z -o CHANGELOG.md
231
+ ```
232
+
233
+ When a `vX.Y.Z` tag is pushed, the release workflow checks the versions and that CHANGELOG.md has a section for it.
234
+
224
235
  ## License
225
236
 
226
237
  MIT, see [LICENSE](https://github.com/MetaSekaiLab/nnnotes/blob/main/LICENSE).
@@ -191,6 +191,17 @@ everywhere.
191
191
  See [docs/performance.md](https://github.com/MetaSekaiLab/nnnotes/blob/main/docs/performance.md) for where a
192
192
  `web` build spends its time and which parts of it run in compiled code.
193
193
 
194
+ ## Changelog
195
+
196
+ [CHANGELOG.md](https://github.com/MetaSekaiLab/nnnotes/blob/main/CHANGELOG.md) lists the changes in every version,
197
+ generated from the commits by [git-cliff](https://git-cliff.org/). The commit that bumps the version regenerates it:
198
+
199
+ ```bash
200
+ git cliff --tag vX.Y.Z -o CHANGELOG.md
201
+ ```
202
+
203
+ When a `vX.Y.Z` tag is pushed, the release workflow checks the versions and that CHANGELOG.md has a section for it.
204
+
194
205
  ## License
195
206
 
196
207
  MIT, see [LICENSE](https://github.com/MetaSekaiLab/nnnotes/blob/main/LICENSE).
@@ -38,13 +38,14 @@ Source = "https://github.com/MetaSekaiLab/nnnotes"
38
38
  Issues = "https://github.com/MetaSekaiLab/nnnotes/issues"
39
39
 
40
40
  [tool.maturin]
41
- # the pure Python package src/nnnotes with the extension module nnnotes._deck (rust/: ournotes-deck, pinned by
42
- # commit); the version is rust/Cargo.toml's, kept equal to nnnotes.__version__ (tests/test_musicdata.py)
41
+ # the pure Python package src/nnnotes with the extension module nnnotes._deck (rust/: ournotes-sim of ournotes-deck,
42
+ # pinned by commit); the version is rust/Cargo.toml's, kept equal to nnnotes.__version__ (tests/test_musicdata.py)
43
43
  python-source = "src"
44
44
  module-name = "nnnotes._deck"
45
45
  manifest-path = "rust/Cargo.toml"
46
46
  profile = "release"
47
47
  locked = true
48
+ include = [{ path = "CHANGELOG.md", format = "sdist" }]
48
49
 
49
50
  [tool.pytest.ini_options]
50
51
  testpaths = ["tests"]
@@ -2,12 +2,64 @@
2
2
  # It is not intended for manual editing.
3
3
  version = 4
4
4
 
5
+ [[package]]
6
+ name = "block-buffer"
7
+ version = "0.12.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
10
+ dependencies = [
11
+ "hybrid-array",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "cfg-if"
16
+ version = "1.0.5"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
19
+
20
+ [[package]]
21
+ name = "cpufeatures"
22
+ version = "0.3.1"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
25
+ dependencies = [
26
+ "libc",
27
+ ]
28
+
29
+ [[package]]
30
+ name = "crypto-common"
31
+ version = "0.2.2"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
34
+ dependencies = [
35
+ "hybrid-array",
36
+ ]
37
+
38
+ [[package]]
39
+ name = "digest"
40
+ version = "0.11.3"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
43
+ dependencies = [
44
+ "block-buffer",
45
+ "crypto-common",
46
+ ]
47
+
5
48
  [[package]]
6
49
  name = "heck"
7
50
  version = "0.5.0"
8
51
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
52
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
10
53
 
54
+ [[package]]
55
+ name = "hybrid-array"
56
+ version = "0.4.15"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
59
+ dependencies = [
60
+ "typenum",
61
+ ]
62
+
11
63
  [[package]]
12
64
  name = "itoa"
13
65
  version = "1.0.18"
@@ -28,9 +80,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
28
80
 
29
81
  [[package]]
30
82
  name = "nnnotes-deck"
31
- version = "0.1.3"
83
+ version = "0.1.4"
32
84
  dependencies = [
33
- "ournotes-deck",
85
+ "ournotes-sim",
34
86
  "pyo3",
35
87
  "serde_json",
36
88
  ]
@@ -42,12 +94,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
42
94
  checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
43
95
 
44
96
  [[package]]
45
- name = "ournotes-deck"
46
- version = "0.0.1"
47
- source = "git+https://github.com/empty-sekai/ournotes-deck?rev=dbd9cf01a4854808dceb6aedcd4041af372faeee#dbd9cf01a4854808dceb6aedcd4041af372faeee"
97
+ name = "ournotes-sim"
98
+ version = "0.0.2"
99
+ source = "git+https://github.com/empty-sekai/ournotes-deck?rev=76e96b617842bdcc031cce98e3dfeead27ddbb85#76e96b617842bdcc031cce98e3dfeead27ddbb85"
48
100
  dependencies = [
49
101
  "serde",
50
102
  "serde_json",
103
+ "sha2",
51
104
  ]
52
105
 
53
106
  [[package]]
@@ -174,6 +227,17 @@ dependencies = [
174
227
  "zmij",
175
228
  ]
176
229
 
230
+ [[package]]
231
+ name = "sha2"
232
+ version = "0.11.0"
233
+ source = "registry+https://github.com/rust-lang/crates.io-index"
234
+ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
235
+ dependencies = [
236
+ "cfg-if",
237
+ "cpufeatures",
238
+ "digest",
239
+ ]
240
+
177
241
  [[package]]
178
242
  name = "syn"
179
243
  version = "2.0.119"
@@ -202,6 +266,12 @@ version = "0.13.5"
202
266
  source = "registry+https://github.com/rust-lang/crates.io-index"
203
267
  checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
204
268
 
269
+ [[package]]
270
+ name = "typenum"
271
+ version = "1.20.1"
272
+ source = "registry+https://github.com/rust-lang/crates.io-index"
273
+ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
274
+
205
275
  [[package]]
206
276
  name = "unicode-ident"
207
277
  version = "1.0.26"
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "nnnotes-deck"
3
+ # the nnnotes version (src/nnnotes/__init__.py); the wheel takes its version from here
4
+ version = "0.1.4"
5
+ edition = "2024"
6
+ rust-version = "1.85"
7
+ description = "nnnotes._deck: the ournotes-sim chart statistics as a Python extension module"
8
+ license = "MIT"
9
+ publish = false
10
+
11
+ [lib]
12
+ name = "_deck"
13
+ crate-type = ["cdylib"]
14
+
15
+ [dependencies]
16
+ pyo3 = { version = "0.29", features = ["abi3-py311"] }
17
+ # the deck model, the crate ournotes-sim of the ournotes-deck repository, pinned to the commit of a release;
18
+ # .github/workflows/deck.yml opens a pull request when ournotes-deck publishes a newer release
19
+ ournotes-sim = { git = "https://github.com/empty-sekai/ournotes-deck", rev = "76e96b617842bdcc031cce98e3dfeead27ddbb85" }
20
+ serde_json = { version = "1", features = ["raw_value", "arbitrary_precision"] }
21
+
22
+ [profile.release]
23
+ lto = "thin"
24
+ codegen-units = 1
@@ -1,5 +1,5 @@
1
- //! Records the ournotes-deck package the module is built with (version and git commit, from Cargo.lock) as the
2
- //! environment variables DECK_VERSION, DECK_SOURCE and DECK_COMMIT of the crate.
1
+ //! Records the ournotes-sim package the module is built with (version, repository and git commit, from Cargo.lock)
2
+ //! as the environment variables DECK_VERSION, DECK_SOURCE and DECK_COMMIT of the crate.
3
3
 
4
4
  use std::path::Path;
5
5
 
@@ -16,15 +16,15 @@ fn main() {
16
16
  Some(v.trim_matches('"').to_string())
17
17
  })
18
18
  };
19
- if field("name").as_deref() == Some("ournotes-deck") {
19
+ if field("name").as_deref() == Some("ournotes-sim") {
20
20
  version = field("version");
21
21
  source = field("source");
22
22
  }
23
23
  }
24
- let version = version.expect("Cargo.lock has no ournotes-deck package");
25
- let source = source.expect("ournotes-deck has no source in Cargo.lock");
24
+ let version = version.expect("Cargo.lock has no ournotes-sim package");
25
+ let source = source.expect("ournotes-sim has no source in Cargo.lock");
26
26
  // git+https://github.com/empty-sekai/ournotes-deck?rev=<rev>#<commit>
27
- let (url, commit) = source.split_once('#').expect("ournotes-deck is not a git dependency");
27
+ let (url, commit) = source.split_once('#').expect("ournotes-sim is not a git dependency");
28
28
  let url = url.strip_prefix("git+").unwrap_or(url);
29
29
  let url = url.split_once('?').map_or(url, |(u, _)| u);
30
30
  println!("cargo:rustc-env=DECK_VERSION={version}");
@@ -1,12 +1,12 @@
1
- //! `nnnotes._deck`: the chart statistics of ournotes-deck (`ournotes-deck.chart-stats/2`) on a deck data document
2
- //! held in memory, the charts measured in parallel.
1
+ //! `nnnotes._deck`: the chart statistics of the deck model ournotes-sim (`ournotes-deck.chart-stats/2`) on a deck
2
+ //! data document held in memory, the charts measured in parallel.
3
3
 
4
4
  use std::sync::Mutex;
5
5
  use std::sync::atomic::{AtomicUsize, Ordering};
6
6
 
7
- use ournotes_deck::chartstats::{self, AptitudeOptions, ChartStats, GEKISOU_SEEDS, Options};
8
- use ournotes_deck::data::DeckData;
9
- use ournotes_deck::error::Error;
7
+ use ournotes_sim::chartstats::{self, AptitudeOptions, ChartStats, GEKISOU_SEEDS, Options};
8
+ use ournotes_sim::data::DeckData;
9
+ use ournotes_sim::error::Error;
10
10
  use pyo3::exceptions::PyValueError;
11
11
  use pyo3::prelude::*;
12
12
  use pyo3::types::PyDict;
@@ -94,8 +94,8 @@ fn chart_stats(
94
94
  })
95
95
  }
96
96
 
97
- /// info() -> dict: the ournotes-deck package of this module (name, version, source, commit) and the formats it reads
98
- /// and writes.
97
+ /// info() -> dict: the deck model of this module (name: the ournotes-deck repository; version, source and commit:
98
+ /// its crate ournotes-sim as built) and the formats it reads and writes.
99
99
  #[pyfunction]
100
100
  fn info(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
101
101
  let d = PyDict::new(py);
@@ -103,7 +103,7 @@ fn info(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
103
103
  d.set_item("version", env!("DECK_VERSION"))?;
104
104
  d.set_item("source", env!("DECK_SOURCE"))?;
105
105
  d.set_item("commit", env!("DECK_COMMIT"))?;
106
- d.set_item("dataFormat", ournotes_deck::data::FORMAT)?;
106
+ d.set_item("dataFormat", ournotes_sim::data::FORMAT)?;
107
107
  d.set_item("format", chartstats::FORMAT)?;
108
108
  Ok(d)
109
109
  }
@@ -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.3"
7
+ __version__ = "0.1.4"
@@ -693,7 +693,8 @@ def cmd_music_data(args, cfg):
693
693
  catalog=deckdata.catalog_info(cat, cli_assets.store_root(args, cfg)),
694
694
  deck=deck, full=args.full,
695
695
  jacket=musicdata.catalog_jacket(cat) if args.jackets else None,
696
- jackets_dir=args.jackets, replay_dir=args.replay_dir, replay_engine=args.replay_engine)
696
+ jackets_dir=args.jackets, replay_dir=args.replay_dir, replay_engine=args.replay_engine,
697
+ recommend_engine=args.recommend_engine)
697
698
  except (deckdata.DeckDataError, musicdata.MusicDataError) as e:
698
699
  sys.exit(f"nnnotes: {e}")
699
700
  _print_json(r)
@@ -1001,8 +1002,12 @@ def build_parser() -> argparse.ArgumentParser:
1001
1002
  help="do not read the BGM cue sheets (every song's bgm.length is null)")
1002
1003
  c.add_argument("--replay-dir", metavar="DIR",
1003
1004
  help="write canonical runtime DeckData, per-chart inputs and replay manifest under the output directory")
1004
- c.add_argument("--replay-engine", metavar="DIR",
1005
- help="copy pinned wasm-bindgen JS/WASM + build.json into --replay-dir")
1005
+ c.add_argument("--replay-engine", metavar="PKG",
1006
+ help="the deck model's replay WASM release package (ournotes-replay-wasm-vVERSION.tar.gz or its "
1007
+ "directory); its web JS/WASM and build-info.json are copied into --replay-dir")
1008
+ c.add_argument("--recommend-engine", metavar="PKG",
1009
+ help="the deck model's recommendation WASM release package (ournotes-recommend-wasm-vVERSION.tar.gz "
1010
+ "or its directory); its web JS/WASM and build-info.json are copied into --replay-dir")
1006
1011
  c.add_argument("--jackets", metavar="DIR",
1007
1012
  help="also write every song's jacket as DIR/<jacket>.webp (at most 320 px on the longer side)")
1008
1013
  _out(c, "output file (.json, or .json.gz for gzip)")
@@ -8,7 +8,7 @@ the store.
8
8
  nnnotes plan [the selection and parameters of export] [-o OUT] [--json] [--since RUN] [--check] [--census]
9
9
  [--emit-tasks DIR] [--why TASK]
10
10
  nnnotes run-stage TASK.json|DIR [...] [--store DIR] [--fetch] [--force]
11
- nnnotes catalogs list [--json] | import FILE [--label L] | fetch [--label L] | diff A B [--json]
11
+ nnnotes catalogs list [--json] | import FILE [--label L] [--resource-version V] | fetch [--label L] | diff A B [--json]
12
12
  nnnotes store verify [--quick]
13
13
 
14
14
  The pipeline is the registry STAGE_SOURCES in order: each entry names the module and attribute of a stage (a Stage
@@ -47,6 +47,7 @@ from __future__ import annotations
47
47
  import argparse
48
48
  import fnmatch
49
49
  import json
50
+ import re
50
51
  import sys
51
52
  import threading
52
53
  import zipfile
@@ -60,7 +61,7 @@ from types import SimpleNamespace
60
61
 
61
62
  from .apkset import ApkSet
62
63
  from . import contract
63
- from .config import ConfigError, describe as describe_setting, usable_cpus, use
64
+ from .config import ConfigError, check_catalog_version, describe as describe_setting, usable_cpus, use
64
65
  from .contract import Cost, IncompatibleTask, Input, Task
65
66
  from .stages import Env, Pending, Stage, describe, execute
66
67
 
@@ -1335,6 +1336,28 @@ def _apk_catalog(args, cfg) -> bytes | None:
1335
1336
  return apk_catalog(apk)
1336
1337
 
1337
1338
 
1339
+ CATALOG_FILE = re.compile(r"catalog_([A-Za-z0-9][A-Za-z0-9.-]*)_[A-Za-z0-9-]+\.bin")
1340
+
1341
+
1342
+ def _import_resource_version(given: str | None, file: Path, source: dict | None) -> str | None:
1343
+ """The resource version of an imported catalog: `given` (--resource-version), else the JP source's version, else
1344
+ the version in an international file name `catalog_<version>_<language>.bin` (none for `main`). A JP source is
1345
+ authoritative: a different `given` is an error."""
1346
+ if given is not None:
1347
+ try:
1348
+ check_catalog_version(given)
1349
+ except ValueError as e:
1350
+ raise ValueError(f"--resource-version {given!r}: {e}") from None
1351
+ if source is not None:
1352
+ if given is not None and given != source["version"]:
1353
+ raise ValueError(f"--resource-version {given} differs from the catalog's source version {source['version']}")
1354
+ return source["version"]
1355
+ if given is not None:
1356
+ return given
1357
+ m = CATALOG_FILE.fullmatch(file.name)
1358
+ return m[1] if m and m[1] != "main" and ".." not in m[1] else None
1359
+
1360
+
1338
1361
  def cmd_catalogs_import(args, cfg, common):
1339
1362
  try:
1340
1363
  remote = Path(args.file).read_bytes()
@@ -1345,9 +1368,10 @@ def cmd_catalogs_import(args, cfg, common):
1345
1368
  if cfg.provider() == "jp":
1346
1369
  from .jp import read_source
1347
1370
  source = read_source(args.file, remote).to_dict()
1371
+ resource = _import_resource_version(args.resource_version, Path(args.file), source)
1348
1372
  v = _db(args, cfg).add(remote, _apk_catalog(args, cfg), label=args.label,
1349
1373
  region=cfg.get("catalog", "region"), language=cfg.get("catalog", "language"),
1350
- resource_version=args.resource_version,
1374
+ resource_version=resource,
1351
1375
  apk_version_name=_apk_version(cfg.path("paths", "apk")), source=source)
1352
1376
  except ValueError as e:
1353
1377
  args.usage(str(e))
@@ -1525,7 +1549,9 @@ def register(sub, common=None) -> None:
1525
1549
  m.add_argument("file", help="catalog_<version>_<language>.bin")
1526
1550
  m.add_argument("--label", help="label of the version (default: its resource version, else the sha256 prefix)")
1527
1551
  m.add_argument("--apk-catalog", help="the APK's catalog.bin (default: read from [paths] apk)")
1528
- m.add_argument("--resource-version", help="the game's resource version of this catalog")
1552
+ m.add_argument("--resource-version",
1553
+ help="the game's resource version of this catalog (default: the JP source's version, else the "
1554
+ "version in the file name catalog_<version>_<language>.bin)")
1529
1555
  _store_arg(m)
1530
1556
  m.set_defaults(func=bind(cmd_catalogs_import), usage=m.error)
1531
1557
  m = csub.add_parser("fetch", help="fetch the region's current catalog and import it")
@@ -30,7 +30,7 @@ from pathlib import Path
30
30
  from .apkset import ApkSet
31
31
  from typing import Callable
32
32
 
33
- DECK_FORMAT = "nnnotes.deck-data/1" # the deck model's input format (ournotes-deck data::FORMAT)
33
+ DECK_FORMAT = "nnnotes.deck-data/1" # the deck model's input format (ournotes-sim data::FORMAT)
34
34
  CHART_FORMAT = "nnnotes.live-score/1" # score.convert's format: the converter the notes come from
35
35
  CHART_PREFIX = "Live/MusicScore/" # + MasterLiveMusicScore._musicScoreTextFileName
36
36
  MANIFEST = "MasterManifest.json"
@@ -63,11 +63,12 @@ TABLES: tuple[tuple[str, tuple[str, ...] | None], ...] = tuple(
63
63
  "_supportSkill02Level _gekisouSupportSkill01Level _gekisouSupportSkill02Level"),
64
64
  ("MasterCharacter", "_id _bandID"),
65
65
  ("MasterBand", "_id"),
66
- ("MasterCharacterRank", "_id _rank _bonus"),
66
+ ("MasterCharacterRank", "_id _rank _exp _bonus"),
67
67
  ("MasterCharacterTotalRank", "_id _totalRank _bonus"),
68
68
  ("MasterBandItemSkillEffect", "_id _bandItemId _level _skillTargetIDs _skillEffectType _effectValue"),
69
69
  ("MasterBandItem", "_id _bandId"),
70
70
  ("MasterBandItemLevel", "_id _bandItemId _level _playerRank"),
71
+ ("MasterVip", "_id _vipRank"),
71
72
  ("MasterVipRankBonus", "_id _vipRank _vipBonusType _value"),
72
73
  ("MasterMemoryMusicGroup", "_id _skillTargetIds"),
73
74
  ("MasterMemoryMusic", "_id _groupId"),
@@ -415,7 +415,7 @@ def _check_rank_check(rc: dict, n: int, s: str, where: str) -> None:
415
415
  _check_bound(rc, f"{s}: the check deck at ranks {rc['ranks']!r}", where)
416
416
 
417
417
 
418
- # ournotes-deck's published seed set (live::seeds): candidate k is the low 32 bits (a signed integer) of output k + 1
418
+ # ournotes-sim's published seed set (live::seeds): candidate k is the low 32 bits (a signed integer) of output k + 1
419
419
  # of SplitMix64 started at SEED_ORIGIN (the ASCII bytes of `gekisou1`); a candidate is kept when its pair of effective
420
420
  # stream seeds (|b| and |b ^ 0x9E3779B9|, -2^31 taken as 2^31 - 1) differs from that of every seed kept before it
421
421
  SEED_ORIGIN = 0x6765_6B69_736F_7531
@@ -1090,7 +1090,7 @@ def tables_of(deck: bool, full: bool) -> tuple[str, ...]:
1090
1090
  def export(out, src: deckdata.MasterSource, key, fetch: Callable[[str], bytes],
1091
1091
  bgm: Callable[[str, str], dict] | None, *, region: str, client: dict, catalog: dict,
1092
1092
  deck: Deck | None = None, full: bool = False, jacket: Callable[[str], bytes] | None = None,
1093
- jackets_dir=None, replay_dir=None, replay_engine=None) -> dict:
1093
+ jackets_dir=None, replay_dir=None, replay_engine=None, recommend_engine=None) -> dict:
1094
1094
  """Read the master data, every chart and every BGM cue sheet, measure the charts with `deck`, then write the file
1095
1095
  `out` (gzip when it ends in `.gz`) through a temporary file and a rename; with `jacket` and `jackets_dir`, first
1096
1096
  every song's jacket as `<jackets_dir>/<jacket>.webp`. Returns the summary."""
@@ -1099,6 +1099,8 @@ def export(out, src: deckdata.MasterSource, key, fetch: Callable[[str], bytes],
1099
1099
  try:
1100
1100
  if replay_engine is not None and replay_dir is None:
1101
1101
  raise deckdata.DeckDataError("--replay-engine needs --replay-dir")
1102
+ if recommend_engine is not None and replay_dir is None:
1103
+ raise deckdata.DeckDataError("--recommend-engine needs --replay-dir")
1102
1104
  if replay_dir is not None and bgm is None:
1103
1105
  raise deckdata.DeckDataError("replay export requires actual BGM lengths; --no-bgm is incompatible")
1104
1106
  tables, shas = deckdata.read_master(src, key, tables_of(deck is not None, full or replay_dir is not None))
@@ -1107,7 +1109,7 @@ def export(out, src: deckdata.MasterSource, key, fetch: Callable[[str], bytes],
1107
1109
  replay_files = None
1108
1110
  if replay_dir is not None:
1109
1111
  from . import replaydata
1110
- replay_files, replay_manifest = replaydata.bundle(doc, replay_engine)
1112
+ replay_files, replay_manifest = replaydata.bundle(doc, replay_engine, recommend_engine)
1111
1113
  manifest_sha = hashlib.sha256(replay_files["manifest.json"]).hexdigest()
1112
1114
  replay_target = Path(replay_dir) / manifest_sha
1113
1115
  try:
@@ -0,0 +1,138 @@
1
+ """Normalized runtime inputs for the shared Rust/WASM replay; no scoring or frame scheduler here."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import tarfile
7
+ from pathlib import Path
8
+
9
+ from . import deckdata
10
+ from .cache import write_atomic
11
+
12
+ FORMAT = "nnnotes.replay-manifest/1"
13
+ CHART_FORMAT = "nnnotes.replay-chart/1"
14
+ BUILD_INFO = "build-info.json"
15
+
16
+
17
+ def _json(value) -> bytes:
18
+ return deckdata.encode(deckdata._value(value, "replay input"))
19
+
20
+
21
+ def _read_package(source: Path, paths: tuple[str, ...], name: str) -> dict[str, bytes]:
22
+ """Those of the files `paths` that a release package holds: a directory, or a .tar.gz archive holding the package
23
+ directory."""
24
+ if source.is_dir():
25
+ return {path: (source / path).read_bytes() for path in paths if (source / path).is_file()}
26
+ try:
27
+ with tarfile.open(source, "r:gz") as archive:
28
+ return {member.name.partition("/")[2]: archive.extractfile(member).read()
29
+ for member in archive.getmembers() if member.isfile() and member.name.partition("/")[2] in paths}
30
+ except (OSError, tarfile.TarError):
31
+ raise deckdata.DeckDataError(
32
+ f"{name}: {source} is neither a package directory nor a .tar.gz package") from None
33
+
34
+
35
+ def _package(files: dict[str, bytes], source: Path, model: dict | None, module: str,
36
+ prefix: str) -> tuple[str, str, str]:
37
+ """Copy the web binding of an ournotes-deck WASM release package of `module` (`replay` or `recommend`), the
38
+ `ournotes-<module>-wasm-v<version>` .tar.gz or its directory, into `files` under `prefix`. Its build-info.json must
39
+ describe the WASM package of `module` built from the pinned model's commit, with the SHA-256 of both files.
40
+ Returns the JS, WASM and build-info.json paths."""
41
+ name = f"{module} engine"
42
+ stem = f"web/ournotes_{module}_wasm"
43
+ js, wasm = f"{stem}.js", f"{stem}_bg.wasm"
44
+ package = _read_package(Path(source), (BUILD_INFO, js, wasm), name)
45
+ if not model or not model.get("commit"):
46
+ raise deckdata.DeckDataError(f"{name}: the pinned deck model identity is required")
47
+ if BUILD_INFO not in package:
48
+ raise deckdata.DeckDataError(f"{name}: missing {BUILD_INFO} in {source}")
49
+ try:
50
+ info = json.loads(package[BUILD_INFO])
51
+ except ValueError:
52
+ raise deckdata.DeckDataError(f"{name}: malformed {BUILD_INFO}") from None
53
+ if not isinstance(info, dict) or info.get("kind") != "wasm" or info.get("module") != module:
54
+ raise deckdata.DeckDataError(f"{name}: {BUILD_INFO} does not describe the {module} WASM package")
55
+ if info.get("commit") != model["commit"]:
56
+ raise deckdata.DeckDataError(f"{name}: {BUILD_INFO} commit {info.get('commit')!r} differs from the pinned "
57
+ f"deck model {model['commit']}")
58
+ hashes = info.get("files") if isinstance(info.get("files"), dict) else {}
59
+ for path in (js, wasm):
60
+ if path not in package:
61
+ raise deckdata.DeckDataError(f"{name}: missing {path} in {source}")
62
+ if hashes.get(path) != hashlib.sha256(package[path]).hexdigest():
63
+ raise deckdata.DeckDataError(f"{name}: {path} SHA-256 differs from {BUILD_INFO}")
64
+ if not package[wasm].startswith(b"\x00asm\x01\x00\x00\x00"):
65
+ raise deckdata.DeckDataError(f"{name}: not a WASM v1 module")
66
+ paths = tuple(f"{prefix}/{path.rsplit('/', 1)[-1]}" for path in (js, wasm, BUILD_INFO))
67
+ for path, data in zip(paths, (package[js], package[wasm], package[BUILD_INFO])):
68
+ files[path] = data
69
+ return paths
70
+
71
+
72
+ def bundle(music: dict, engine_dir: Path | None = None,
73
+ recommend_dir: Path | None = None) -> tuple[dict[str, bytes], dict]:
74
+ """Canonical normalized --full inputs and SHA manifest. Actual ACB length is mandatory.
75
+
76
+ Runtime arrays retain native enumeration order. ReplaySession.template/run in Rust produce frames and scores.
77
+ """
78
+ if "master" not in music or "charts" not in music:
79
+ raise deckdata.DeckDataError("replay input needs the normalized --full master/charts")
80
+ lengths = {}
81
+ for song in music["songs"]:
82
+ ms = ((song.get("bgm") or {}).get("length") or {}).get("lengthMs")
83
+ if type(ms) is not int or ms <= 0:
84
+ raise deckdata.DeckDataError(f"replay song {song['id']}: a positive actual BGM lengthMs is required")
85
+ for chart in song["charts"]:
86
+ key = str(chart["scoreId"])
87
+ if key in lengths:
88
+ raise deckdata.DeckDataError(f"replay chart {key}: duplicate song chart")
89
+ lengths[key] = ms
90
+ provenance = dict(music["provenance"])
91
+ provenance["replay"] = {"musicLengthsMs": lengths,
92
+ "musicLengthSource": "ACB CueTable lengthMs; no last-note estimate"}
93
+ source_records = sorted(music["charts"], key=lambda chart: chart["scoreId"])
94
+ records = [c for c in source_records if str(c["scoreId"]) in lengths]
95
+ if len(records) != len(lengths) or {str(c["scoreId"]) for c in records} != set(lengths):
96
+ raise deckdata.DeckDataError("replay runtime charts differ from the songs' charts")
97
+ data = {"format": deckdata.DECK_FORMAT, "provenance": provenance,
98
+ "master": music["master"], "charts": records}
99
+ files = {"deck-data.json": _json(data)}
100
+
101
+ def resource(path: str) -> dict:
102
+ return {"url": path, "sha256": hashlib.sha256(files[path]).hexdigest(), "bytes": len(files[path])}
103
+
104
+ charts = []
105
+ for record in records:
106
+ sid = record["scoreId"]
107
+ path = f"charts/{sid}.json"
108
+ files[path] = _json({"format": CHART_FORMAT, "scoreId": sid,
109
+ "musicLengthMs": lengths[str(sid)], "chart": record})
110
+ charts.append({"scoreId": sid, **resource(path), "musicLengthMs": lengths[str(sid)],
111
+ "noteCount": len(record["notes"]["id"]), "assetSha256": record["asset"]["sha256"]})
112
+ model = music["provenance"].get("deck")
113
+ engine = None
114
+ if engine_dir is not None:
115
+ js, wasm, build = _package(files, engine_dir, model, "replay", "engine")
116
+ engine = {"model": model, "requestFormat": "ournotes.replay/1", "class": "ReplaySession",
117
+ "methods": ["describeChart", "template", "run"],
118
+ "js": resource(js), "wasm": resource(wasm), "build": resource(build)}
119
+ recommend = None
120
+ if recommend_dir is not None:
121
+ js, wasm, build = _package(files, recommend_dir, model, "recommend", "recommend")
122
+ recommend = {"model": model, "js": resource(js), "wasm": resource(wasm), "build": resource(build)}
123
+ manifest = {"format": FORMAT, "deckData": {"format": deckdata.DECK_FORMAT, **resource("deck-data.json")},
124
+ "charts": charts, "engine": engine,
125
+ **({"recommendEngine": recommend} if recommend is not None else {}),
126
+ "unlistedScoreIds": [c["scoreId"] for c in source_records if str(c["scoreId"]) not in lengths],
127
+ "clock": "Explicit frames from ReplaySession.template; no Python/JS scoring or scheduling"}
128
+ files["manifest.json"] = _json(manifest)
129
+ return files, manifest
130
+
131
+
132
+ def write(directory: Path, files: dict[str, bytes]) -> None:
133
+ """Write validated normalized artifacts; callers publish the marker last."""
134
+ directory = Path(directory)
135
+ for name, data in files.items():
136
+ path = directory / name
137
+ path.parent.mkdir(parents=True, exist_ok=True)
138
+ write_atomic(path, data)
@@ -1,23 +0,0 @@
1
- [package]
2
- name = "nnnotes-deck"
3
- # the nnnotes version (src/nnnotes/__init__.py); the wheel takes its version from here
4
- version = "0.1.3"
5
- edition = "2024"
6
- rust-version = "1.85"
7
- description = "nnnotes._deck: the ournotes-deck chart statistics as a Python extension module"
8
- license = "MIT"
9
- publish = false
10
-
11
- [lib]
12
- name = "_deck"
13
- crate-type = ["cdylib"]
14
-
15
- [dependencies]
16
- pyo3 = { version = "0.29", features = ["abi3-py311"] }
17
- # pinned by commit; .github/workflows/deck.yml opens a pull request when ournotes-deck moves
18
- ournotes-deck = { git = "https://github.com/empty-sekai/ournotes-deck", rev = "dbd9cf01a4854808dceb6aedcd4041af372faeee" }
19
- serde_json = { version = "1", features = ["raw_value", "arbitrary_precision"] }
20
-
21
- [profile.release]
22
- lto = "thin"
23
- codegen-units = 1
@@ -1,101 +0,0 @@
1
- """Normalized runtime inputs for the shared Rust/WASM replay; no scoring or frame scheduler here."""
2
- from __future__ import annotations
3
-
4
- import hashlib
5
- import json
6
- from pathlib import Path
7
-
8
- from . import deckdata
9
- from .cache import write_atomic
10
-
11
- FORMAT = "nnnotes.replay-manifest/1"
12
- CHART_FORMAT = "nnnotes.replay-chart/1"
13
- JS = "ournotes_replay.js"
14
- WASM = "ournotes_replay_bg.wasm"
15
-
16
-
17
- def _json(value) -> bytes:
18
- return deckdata.encode(deckdata._value(value, "replay input"))
19
-
20
-
21
- def bundle(music: dict, engine_dir: Path | None = None) -> tuple[dict[str, bytes], dict]:
22
- """Canonical normalized --full inputs and SHA manifest. Actual ACB length is mandatory.
23
-
24
- Runtime arrays retain native enumeration order. ReplaySession.template/run in Rust produce frames and scores.
25
- """
26
- if "master" not in music or "charts" not in music:
27
- raise deckdata.DeckDataError("replay input needs the normalized --full master/charts")
28
- lengths = {}
29
- for song in music["songs"]:
30
- ms = ((song.get("bgm") or {}).get("length") or {}).get("lengthMs")
31
- if type(ms) is not int or ms <= 0:
32
- raise deckdata.DeckDataError(f"replay song {song['id']}: a positive actual BGM lengthMs is required")
33
- for chart in song["charts"]:
34
- key = str(chart["scoreId"])
35
- if key in lengths:
36
- raise deckdata.DeckDataError(f"replay chart {key}: duplicate song chart")
37
- lengths[key] = ms
38
- provenance = dict(music["provenance"])
39
- provenance["replay"] = {"musicLengthsMs": lengths,
40
- "musicLengthSource": "ACB CueTable lengthMs; no last-note estimate"}
41
- source_records = sorted(music["charts"], key=lambda chart: chart["scoreId"])
42
- records = [c for c in source_records if str(c["scoreId"]) in lengths]
43
- if len(records) != len(lengths) or {str(c["scoreId"]) for c in records} != set(lengths):
44
- raise deckdata.DeckDataError("replay runtime charts differ from the songs' charts")
45
- data = {"format": deckdata.DECK_FORMAT, "provenance": provenance,
46
- "master": music["master"], "charts": records}
47
- files = {"deck-data.json": _json(data)}
48
-
49
- def resource(path: str) -> dict:
50
- return {"url": path, "sha256": hashlib.sha256(files[path]).hexdigest(), "bytes": len(files[path])}
51
-
52
- charts = []
53
- for record in records:
54
- sid = record["scoreId"]
55
- path = f"charts/{sid}.json"
56
- files[path] = _json({"format": CHART_FORMAT, "scoreId": sid,
57
- "musicLengthMs": lengths[str(sid)], "chart": record})
58
- charts.append({"scoreId": sid, **resource(path), "musicLengthMs": lengths[str(sid)],
59
- "noteCount": len(record["notes"]["id"]), "assetSha256": record["asset"]["sha256"]})
60
- model = music["provenance"].get("deck")
61
- engine = None
62
- if engine_dir is not None:
63
- engine_dir = Path(engine_dir)
64
- try:
65
- built = json.loads((engine_dir / "build.json").read_text(encoding="utf8"))
66
- except (OSError, ValueError):
67
- raise deckdata.DeckDataError("replay engine: missing or malformed build.json") from None
68
- if built.get("format") != "ournotes.replay-engine/1" or not model or built.get("commit") != model.get("commit"):
69
- raise deckdata.DeckDataError("replay engine: build.json commit differs from the pinned deck model")
70
- for name in (JS, WASM):
71
- source = engine_dir / name
72
- if not source.is_file():
73
- raise deckdata.DeckDataError(f"replay engine: missing {name} in {engine_dir}")
74
- files[f"engine/{name}"] = source.read_bytes()
75
- if not files[f"engine/{WASM}"].startswith(b"\x00asm\x01\x00\x00\x00"):
76
- raise deckdata.DeckDataError("replay engine: not a WASM v1 module")
77
- if not model or not model.get("commit"):
78
- raise deckdata.DeckDataError("replay engine: the pinned deck model identity is required")
79
- if built.get("jsSha256") != hashlib.sha256(files[f"engine/{JS}"]).hexdigest() or \
80
- built.get("wasmSha256") != hashlib.sha256(files[f"engine/{WASM}"]).hexdigest():
81
- raise deckdata.DeckDataError("replay engine: JS/WASM SHA differs from build.json")
82
- files["engine/build.json"] = _json(built)
83
- engine = {"model": model, "requestFormat": "ournotes.replay/1", "class": "ReplaySession",
84
- "methods": ["describeChart", "template", "run"],
85
- "js": resource(f"engine/{JS}"), "wasm": resource(f"engine/{WASM}"),
86
- "build": resource("engine/build.json")}
87
- manifest = {"format": FORMAT, "deckData": {"format": deckdata.DECK_FORMAT, **resource("deck-data.json")},
88
- "charts": charts, "engine": engine,
89
- "unlistedScoreIds": [c["scoreId"] for c in source_records if str(c["scoreId"]) not in lengths],
90
- "clock": "Explicit frames from ReplaySession.template; no Python/JS scoring or scheduling"}
91
- files["manifest.json"] = _json(manifest)
92
- return files, manifest
93
-
94
-
95
- def write(directory: Path, files: dict[str, bytes]) -> None:
96
- """Write validated normalized artifacts; callers publish the marker last."""
97
- directory = Path(directory)
98
- for name, data in files.items():
99
- path = directory / name
100
- path.parent.mkdir(parents=True, exist_ok=True)
101
- write_atomic(path, data)
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