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.
- nnnotes-0.1.4/CHANGELOG.md +161 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/PKG-INFO +12 -1
- {nnnotes-0.1.3 → nnnotes-0.1.4}/README.en.md +11 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/pyproject.toml +3 -2
- {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/Cargo.lock +75 -5
- nnnotes-0.1.4/rust/Cargo.toml +24 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/build.rs +6 -6
- {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/src/lib.rs +8 -8
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/__init__.py +1 -1
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cli.py +8 -3
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cli_assets.py +30 -4
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/deckdata.py +3 -2
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/musicdata.py +5 -3
- nnnotes-0.1.4/src/nnnotes/replaydata.py +138 -0
- nnnotes-0.1.3/rust/Cargo.toml +0 -23
- nnnotes-0.1.3/src/nnnotes/replaydata.py +0 -101
- {nnnotes-0.1.3 → nnnotes-0.1.4}/LICENSE +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/rust/rustfmt.toml +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/__main__.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/acb.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/addressables.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/adv.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advcommand.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advmedia.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advscene.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advui.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/advvideo.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/apkset.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/__init__.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/astc.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/gltf.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/mesh.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/png.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/reader.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/sniff.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/sprite.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/atoms/texture.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cache.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/catalog.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/catalogdb.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/census.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/compress.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/config.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/configfile.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/contract.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cri.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/crikey.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/crilips.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/cristages.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/export.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/gameapi.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/jp.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/jsonio.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/languages.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/layout.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/link.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/live.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/live2d.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveaudio.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/livenotes.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveoptions.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/livescene.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/liveui.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/master.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/modelstages.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/motion.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/nnnotes.example.toml +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/objexport.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/orchestrate.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/plan.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/player.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/room.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/score.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/shader.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/spot.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/stages.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/store.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/story.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyfonts.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyhost.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storysite.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/storyvoices.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/textstyle.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/tmpfont.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/typetrees/6000.3.12f1.json +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/ui.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/unity.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/viewrules.json +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/views.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/voices.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/web.py +0 -0
- {nnnotes-0.1.3 → nnnotes-0.1.4}/src/nnnotes/webaudio.py +0 -0
- {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
|
+
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,
|
|
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.
|
|
83
|
+
version = "0.1.4"
|
|
32
84
|
dependencies = [
|
|
33
|
-
"ournotes-
|
|
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-
|
|
46
|
-
version = "0.0.
|
|
47
|
-
source = "git+https://github.com/empty-sekai/ournotes-deck?rev=
|
|
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-
|
|
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-
|
|
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-
|
|
25
|
-
let source = source.expect("ournotes-
|
|
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-
|
|
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-
|
|
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
|
|
8
|
-
use
|
|
9
|
-
use
|
|
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
|
|
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",
|
|
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.
|
|
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="
|
|
1005
|
-
help="
|
|
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=
|
|
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",
|
|
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-
|
|
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-
|
|
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)
|
nnnotes-0.1.3/rust/Cargo.toml
DELETED
|
@@ -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
|
|
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
|