sdvx nabla support (#1599)

* feat: add sdvx nabla support

Added VF7 algorithm to sdvx profiles
Added nabla folder
Extended sdvx seed with new charts and decimal rerates

* fix: some comments

* refactor: break InsaneCharRebinds out into its own file

* fix: removed weird chars on sdvx song seeds

* fix: charfix

---------

Co-authored-by: zk <zkldi.dev@gmail.com>
This commit is contained in:
Berk Buturak
2026-06-02 00:21:53 +01:00
committed by GitHub
co-authored by zk
parent 830a19a40d
commit d495d7e99c
15 changed files with 23305 additions and 10900 deletions
+21706 -10817
View File
File diff suppressed because it is too large Load Diff
+276 -16
View File
@@ -49976,6 +49976,19 @@
],
"where": "chart.level = '1'"
},
{
"game": "sdvx",
"id": "F19e65f7a94105487528",
"inactive": false,
"legacyFolderID": "F06c91aacb43ee04f91d4177f9d49d29a6dfc2fc1cfeef36b9f3e34f1284ff878",
"searchTerms": [],
"slug": "1-nabla",
"title": "Level 1 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '1'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2ae1f2b212",
@@ -50028,6 +50041,19 @@
],
"where": "chart.level = '10'"
},
{
"game": "sdvx",
"id": "F19e65f7a942444b4c56",
"inactive": false,
"legacyFolderID": "F970863cec6edbbe00b06f52ec90cc460a73e93375f7533b6920ee332caf62bf3",
"searchTerms": [],
"slug": "10-nabla",
"title": "Level 10 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '10'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a6012f137",
@@ -50080,6 +50106,19 @@
],
"where": "chart.level = '11'"
},
{
"game": "sdvx",
"id": "F19e65f7a9425d117c9f",
"inactive": false,
"legacyFolderID": "F4045668507fd550b4419d7cea09d910dff69344863e76ccaa57d371911993412",
"searchTerms": [],
"slug": "11-nabla",
"title": "Level 11 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '11'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a79f306a2",
@@ -50132,6 +50171,19 @@
],
"where": "chart.level = '12'"
},
{
"game": "sdvx",
"id": "F19e65f7a94235340bef",
"inactive": false,
"legacyFolderID": "F1e79c7a470afc3951b4f83e4609c55464e70d5f149c584eb613dfaa632f8136b",
"searchTerms": [],
"slug": "12-nabla",
"title": "Level 12 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '12'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2acaf7328c",
@@ -50184,6 +50236,19 @@
],
"where": "chart.level = '13'"
},
{
"game": "sdvx",
"id": "F19e65f7a94212d9d5b6",
"inactive": false,
"legacyFolderID": "F4c37c527e97a10aa71cd49319c54724d95e5659d90ddb794cedc5acd02479f31",
"searchTerms": [],
"slug": "13-nabla",
"title": "Level 13 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '13'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a8394786a",
@@ -50236,6 +50301,19 @@
],
"where": "chart.level = '14'"
},
{
"game": "sdvx",
"id": "F19e65f7a942a93b25dd",
"inactive": false,
"legacyFolderID": "F5cbddfe1699d57b247a5ab3cd000e7b2b77b5e970fc2bcf5fe7b0d4aeddd8716",
"searchTerms": [],
"slug": "14-nabla",
"title": "Level 14 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '14'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a25154803",
@@ -50288,6 +50366,19 @@
],
"where": "chart.level = '15'"
},
{
"game": "sdvx",
"id": "F19e65f7a942d12d2bcd",
"inactive": false,
"legacyFolderID": "F9a5bc24a0d80df92beb16a0bd901af37e24a4a82ff934cd210d1fe39580cdfe8",
"searchTerms": [],
"slug": "15-nabla",
"title": "Level 15 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '15'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a942955fb",
@@ -50340,6 +50431,19 @@
],
"where": "chart.level = '16'"
},
{
"game": "sdvx",
"id": "F19e65f7a942225ed289",
"inactive": false,
"legacyFolderID": "F7cb5d43e93e0a7947433629e21cf0bada5b71468925cfd00fa3a5eb6b63e2764",
"searchTerms": [],
"slug": "16-nabla",
"title": "Level 16 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '16'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2ae339891a",
@@ -50364,7 +50468,7 @@
"versionFilter": [
"exceed"
],
"where": "chart.level = '17'"
"where": "chart.level_num >= 17 AND chart.level_num < 18"
},
{
"game": "sdvx",
@@ -50377,7 +50481,7 @@
"versionFilter": [
"heaven"
],
"where": "chart.level = '17'"
"where": "chart.level_num >= 17 AND chart.level_num < 18"
},
{
"game": "sdvx",
@@ -50390,7 +50494,20 @@
"versionFilter": [
"konaste"
],
"where": "chart.level = '17'"
"where": "chart.level_num >= 17 AND chart.level_num < 18"
},
{
"game": "sdvx",
"id": "F19e65f7a942390eb617",
"inactive": false,
"legacyFolderID": "Fa7f8a1b2ae78df26c3331df5f540a040bbd86a1db1b05d739ecdfc670aeb9637",
"searchTerms": [],
"slug": "17-nabla",
"title": "Level 17 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level_num >= 17 AND chart.level_num < 18"
},
{
"game": "sdvx",
@@ -50403,7 +50520,7 @@
"versionFilter": [
"vivid"
],
"where": "chart.level = '17'"
"where": "chart.level_num >= 17 AND chart.level_num < 18"
},
{
"game": "sdvx",
@@ -50416,7 +50533,7 @@
"versionFilter": [
"exceed"
],
"where": "chart.level = '18'"
"where": "chart.level_num >= 18 AND chart.level_num < 19"
},
{
"game": "sdvx",
@@ -50429,7 +50546,7 @@
"versionFilter": [
"heaven"
],
"where": "chart.level = '18'"
"where": "chart.level_num >= 18 AND chart.level_num < 19"
},
{
"game": "sdvx",
@@ -50442,7 +50559,20 @@
"versionFilter": [
"konaste"
],
"where": "chart.level = '18'"
"where": "chart.level_num >= 18 AND chart.level_num < 19"
},
{
"game": "sdvx",
"id": "F19e65f7a942f68fcb77",
"inactive": false,
"legacyFolderID": "F046dcc346fd2040e76fa5c0c3017c5b35efd91a6a8213f0b35c1f192e23d36f9",
"searchTerms": [],
"slug": "18-nabla",
"title": "Level 18 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level_num >= 18 AND chart.level_num < 19"
},
{
"game": "sdvx",
@@ -50455,7 +50585,7 @@
"versionFilter": [
"vivid"
],
"where": "chart.level = '18'"
"where": "chart.level_num >= 18 AND chart.level_num < 19"
},
{
"game": "sdvx",
@@ -50468,7 +50598,7 @@
"versionFilter": [
"exceed"
],
"where": "chart.level = '19'"
"where": "chart.level_num >= 19 AND chart.level_num < 20"
},
{
"game": "sdvx",
@@ -50481,7 +50611,7 @@
"versionFilter": [
"heaven"
],
"where": "chart.level = '19'"
"where": "chart.level_num >= 19 AND chart.level_num < 20"
},
{
"game": "sdvx",
@@ -50494,7 +50624,20 @@
"versionFilter": [
"konaste"
],
"where": "chart.level = '19'"
"where": "chart.level_num >= 19 AND chart.level_num < 20"
},
{
"game": "sdvx",
"id": "F19e65f7a94350f1b2f5",
"inactive": false,
"legacyFolderID": "F6d7a9b50f1839a5e381eeb9f7b7687db32b7ff85dba48a50993e04332af55b70",
"searchTerms": [],
"slug": "19-nabla",
"title": "Level 19 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level_num >= 19 AND chart.level_num < 20"
},
{
"game": "sdvx",
@@ -50507,7 +50650,7 @@
"versionFilter": [
"vivid"
],
"where": "chart.level = '19'"
"where": "chart.level_num >= 19 AND chart.level_num < 20"
},
{
"game": "sdvx",
@@ -50548,6 +50691,19 @@
],
"where": "chart.level = '2'"
},
{
"game": "sdvx",
"id": "F19e65f7a94229f855f6",
"inactive": false,
"legacyFolderID": "Fc13d7cfa4d3c2e40731a344c92f817ef40b4a24dba9acef269afdcf001d963e9",
"searchTerms": [],
"slug": "2-nabla",
"title": "Level 2 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '2'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a22330d01",
@@ -50572,7 +50728,7 @@
"versionFilter": [
"exceed"
],
"where": "chart.level = '20'"
"where": "chart.level_num >= 20 AND chart.level_num < 21"
},
{
"game": "sdvx",
@@ -50585,7 +50741,7 @@
"versionFilter": [
"heaven"
],
"where": "chart.level = '20'"
"where": "chart.level_num >= 20 AND chart.level_num < 21"
},
{
"game": "sdvx",
@@ -50598,7 +50754,20 @@
"versionFilter": [
"konaste"
],
"where": "chart.level = '20'"
"where": "chart.level_num >= 20 AND chart.level_num < 21"
},
{
"game": "sdvx",
"id": "F19e65f7a94315059cf3",
"inactive": false,
"legacyFolderID": "Fc0d1303ba2ab371dc6445463e3fb05000d2bec0b67b9cc677014af06c34ceef3",
"searchTerms": [],
"slug": "20-nabla",
"title": "Level 20 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level_num >= 20 AND chart.level_num < 21"
},
{
"game": "sdvx",
@@ -50611,7 +50780,7 @@
"versionFilter": [
"vivid"
],
"where": "chart.level = '20'"
"where": "chart.level_num >= 20 AND chart.level_num < 21"
},
{
"game": "sdvx",
@@ -50652,6 +50821,19 @@
],
"where": "chart.level = '3'"
},
{
"game": "sdvx",
"id": "F19e65f7a942170986fd",
"inactive": false,
"legacyFolderID": "F85a95f0b1e9d63bbdf3e016262a417f0d2a06250dee0fa504946d166b05ece1e",
"searchTerms": [],
"slug": "3-nabla",
"title": "Level 3 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '3'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a06fc5680",
@@ -50704,6 +50886,19 @@
],
"where": "chart.level = '4'"
},
{
"game": "sdvx",
"id": "F19e65f7a942de718bd5",
"inactive": false,
"legacyFolderID": "F838aad7f1c94023b53a5db8a4451860f3296ffc6b9f57131c9be18fe7aac0e0e",
"searchTerms": [],
"slug": "4-nabla",
"title": "Level 4 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '4'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a22e402eb",
@@ -50756,6 +50951,19 @@
],
"where": "chart.level = '5'"
},
{
"game": "sdvx",
"id": "F19e65f7a94271aae4a7",
"inactive": false,
"legacyFolderID": "Fd7dbe2c07e0b9b837269b705b49a36c3da497ab9b9b5a51ecd4427d3d2f89511",
"searchTerms": [],
"slug": "5-nabla",
"title": "Level 5 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '5'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2abaec1504",
@@ -50808,6 +51016,19 @@
],
"where": "chart.level = '6'"
},
{
"game": "sdvx",
"id": "F19e65f7a942e0ce038a",
"inactive": false,
"legacyFolderID": "F3ebcf8d84db180f6ad0ebfe996080e1ff1f57f297668b43d3275757670d4657f",
"searchTerms": [],
"slug": "6-nabla",
"title": "Level 6 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '6'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a1c419e6a",
@@ -50860,6 +51081,19 @@
],
"where": "chart.level = '7'"
},
{
"game": "sdvx",
"id": "F19e65f7a9429db47f49",
"inactive": false,
"legacyFolderID": "F82e42ab32600c922e03364d9da6ce7fef8c2d16351686231ac71d5b2b4e96cd1",
"searchTerms": [],
"slug": "7-nabla",
"title": "Level 7 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '7'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a59757ac7",
@@ -50912,6 +51146,19 @@
],
"where": "chart.level = '8'"
},
{
"game": "sdvx",
"id": "F19e65f7a942aad71db4",
"inactive": false,
"legacyFolderID": "Fbd0528ee59b36b3d9cf8e77604e8c4de577e02c8f4c2144b1e30155ee3400b2c",
"searchTerms": [],
"slug": "8-nabla",
"title": "Level 8 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '8'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2ad2b8d882",
@@ -50964,6 +51211,19 @@
],
"where": "chart.level = '9'"
},
{
"game": "sdvx",
"id": "F19e65f7a94252a65257",
"inactive": false,
"legacyFolderID": "F7373a1e3b34ab71b72326914721e448441cfad85399f554f951ada07af7e44c3",
"searchTerms": [],
"slug": "9-nabla",
"title": "Level 9 (NABLA)",
"versionFilter": [
"nabla"
],
"where": "chart.level = '9'"
},
{
"game": "sdvx",
"id": "F19d35f0cf2a6fafd1ff",
+587
View File
@@ -28796,5 +28796,592 @@
"metameta_himawaripunch"
],
"title": "めた・メタ?ひまわり*パンチ"
},
{
"altTitles": [],
"artist": "青龍改",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c3636d766c",
"legacySongID": 2434,
"searchTerms": [
"bluediamond_seiryu"
],
"title": "Blue Diamond"
},
{
"altTitles": [],
"artist": "Hommarju",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4337999b3",
"legacySongID": 2440,
"searchTerms": [
"blackout_hommarju"
],
"title": "Black Out"
},
{
"altTitles": [],
"artist": "苺咲べりぃ",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4347a4b9e",
"legacySongID": 2444,
"searchTerms": [
"cakecakencake_maisaki"
],
"title": "CAKE,Cake'n Cake!"
},
{
"altTitles": [],
"artist": "xi",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c43ddb87d6",
"legacySongID": 2438,
"searchTerms": [
"celestialcounq_xi"
],
"title": "Celestial Conquest"
},
{
"altTitles": [],
"artist": "ビートまりお",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4526b47b7",
"legacySongID": 2435,
"searchTerms": [
"newerinnn_beatmario"
],
"title": "Help me, ERINNNNNN!!"
},
{
"altTitles": [],
"artist": "TAKIO feat. 駄々子",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4778216ef",
"legacySongID": 2446,
"searchTerms": [
"kareinaruichigeki_takio"
],
"title": "華麗なる一撃"
},
{
"altTitles": [],
"artist": "天束 vs. siromaru",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4b3c7dff9",
"legacySongID": 2442,
"searchTerms": [
"zany_amatsukasiromaru"
],
"title": "Zany Arcadia \"E\""
},
{
"altTitles": [],
"artist": "irom",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4b5b2c8cb",
"legacySongID": 2436,
"searchTerms": [
"deadlineskg_irom"
],
"title": "デッドライン症候群"
},
{
"altTitles": [],
"artist": "EYEN(seatrus+kuro)",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4c23b926d",
"legacySongID": 2449,
"searchTerms": [
"dangerxloze_eyen"
],
"title": "DANGER XLOZE"
},
{
"altTitles": [],
"artist": "りりぃろっくfeat.ЄИ1GM4&桃寝ちのい",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4c31395e7",
"legacySongID": 2439,
"searchTerms": [
"kushinadahimeni_lilyrock"
],
"title": "稲田姫様に叱られちゃいました# ~ Romantic Shortfall"
},
{
"altTitles": [],
"artist": "Capchii × Uzumaki",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4c52d4116",
"legacySongID": 2448,
"searchTerms": [
"showdawn_capchii"
],
"title": "ShowDawn"
},
{
"altTitles": [],
"artist": "polysha&Ashrount",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4d48846de",
"legacySongID": 2443,
"searchTerms": [
"nextage_polyshaashrount"
],
"title": "ИEXTAGE"
},
{
"altTitles": [],
"artist": "めと(Metomate)",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4d4db856d",
"legacySongID": 2447,
"searchTerms": [
"venisresonance_meto"
],
"title": "Veins Resonance"
},
{
"altTitles": [],
"artist": "Riz",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4d7f4f120",
"legacySongID": 2441,
"searchTerms": [
"massivestrength_riz"
],
"title": "Massive Strength"
},
{
"altTitles": [],
"artist": "テヅカ x otoshi.b",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4e53668db",
"legacySongID": 2445,
"searchTerms": [
"tengeitan_teduka"
],
"title": "天鯨譚"
},
{
"altTitles": [],
"artist": "かめりあ",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c4f36a8a18",
"legacySongID": 2437,
"searchTerms": [
"npororoca_kameria"
],
"title": "ンポロロッカ"
},
{
"altTitles": [],
"artist": "Zekk",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c503b3c8b7",
"legacySongID": 2468,
"searchTerms": [
"9thoutburst_zekk"
],
"title": "9th Outburst"
},
{
"altTitles": [],
"artist": "PHQUASE vs. dj TAKA",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c50f3fa655",
"legacySongID": 2472,
"searchTerms": [
"fixer_phquasedjtaka"
],
"title": "fixer"
},
{
"altTitles": [],
"artist": "qfeileadh",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c518ca991b",
"legacySongID": 2450,
"searchTerms": [
"xelonophobea_qfeileadh"
],
"title": "XELENOPHOEBEA"
},
{
"altTitles": [],
"artist": "cosMo@暴走P feat. 初音ミク",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c51a615844",
"legacySongID": 2476,
"searchTerms": [
"mscrazyspeed_cosmo"
],
"title": "ミスクレイジースピード"
},
{
"altTitles": [],
"artist": "原口沙輔",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5371469fa",
"legacySongID": 2454,
"searchTerms": [
"igaku_haraguchisasuke"
],
"title": "イガク"
},
{
"altTitles": [
"華麗なる!音戯探偵ひなビタ鷸"
],
"artist": "音戯探偵ひなビタ♫",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c537933992",
"legacySongID": 2452,
"searchTerms": [
"kareinaruhinabita_penoreri"
],
"title": "華麗なる!音戯探偵ひなビタ♫"
},
{
"altTitles": [],
"artist": "irucaice feat. 初音ミク",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5382c1dfd",
"legacySongID": 2477,
"searchTerms": [
"angeltest_irucaice"
],
"title": "えんじぇる☆てすと!"
},
{
"altTitles": [],
"artist": "Laur",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c53cefab49",
"legacySongID": 2475,
"searchTerms": [
"baphomet_laur"
],
"title": "Baphomet"
},
{
"altTitles": [],
"artist": "Reku Mochizuki",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c53e900b6f",
"legacySongID": 2473,
"searchTerms": [
"moonlitblue_rekumochizuki"
],
"title": "Moonlit Blue"
},
{
"altTitles": [],
"artist": "RoughSketch feat.ななひら",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c545c52283",
"legacySongID": 2457,
"searchTerms": [
"777_roughsketch"
],
"title": "777 (Vocal ver.)"
},
{
"altTitles": [],
"artist": "wotaku feat. SHIKI",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c549242e80",
"legacySongID": 2455,
"searchTerms": [
"snooze_wotaku"
],
"title": "snooze"
},
{
"altTitles": [],
"artist": "tn-shi",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5699611b6",
"legacySongID": 2456,
"searchTerms": [
"synthesis_tnshi"
],
"title": "Synthesis."
},
{
"altTitles": [],
"artist": "Eagle",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c571157c64",
"legacySongID": 2471,
"searchTerms": [
"glitchnride_eagle"
],
"title": "Glitch N Ride"
},
{
"altTitles": [],
"artist": "muyu",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c57a7d4a82",
"legacySongID": 2474,
"searchTerms": [
"signalize_muyu"
],
"title": "シグナライズ"
},
{
"altTitles": [],
"artist": "なきそ",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c580ffd353",
"legacySongID": 2453,
"searchTerms": [
"imasugurinne_nakiso"
],
"title": "いますぐ輪廻"
},
{
"altTitles": [],
"artist": "吉本おじさん",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5847ae150",
"legacySongID": 2463,
"searchTerms": [
"ohenjimadakana_ojisan"
],
"title": "お返事まだカナ?おじさん構文!"
},
{
"altTitles": [],
"artist": "暁Records",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c58883b77b",
"legacySongID": 2461,
"searchTerms": [
"necomantic_akatsuki"
],
"title": "Necromantic"
},
{
"altTitles": [],
"artist": "Cream puff",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c589d3089e",
"legacySongID": 2470,
"searchTerms": [
"mermaidgirl_creampuff"
],
"title": "Mermaid girl"
},
{
"altTitles": [],
"artist": "森羅万象",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c58f8117d7",
"legacySongID": 2460,
"searchTerms": [
"heartinwonderland_sinrabansyou"
],
"title": "ハートインワンダーランド"
},
{
"altTitles": [],
"artist": "Juggernaut.",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5b37fdc14",
"legacySongID": 2451,
"searchTerms": [
"downwithyourlove_juggernaut"
],
"title": "Down with your Love"
},
{
"altTitles": [],
"artist": "名取さな",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5ba191883",
"legacySongID": 2464,
"searchTerms": [
"mondainaitiripper_natorisana"
],
"title": "モンダイナイトリッパー!"
},
{
"altTitles": [],
"artist": "iconoclasm",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5d9e9be8f",
"legacySongID": 2466,
"searchTerms": [
"votumrmx_iconoclasm"
],
"title": "Votum stellarum -forest #25 RMX-"
},
{
"altTitles": [],
"artist": "m@sumi vs RoughSketch",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5dbf7cdc8",
"legacySongID": 2465,
"searchTerms": [
"ztiria_masumiroughsketch"
],
"title": "Zt!ri△"
},
{
"altTitles": [],
"artist": "PON",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5dd4334b7",
"legacySongID": 2467,
"searchTerms": [
"kouen_pon"
],
"title": "紅焔"
},
{
"altTitles": [],
"artist": "Ice",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5e34d8c0b",
"legacySongID": 2459,
"searchTerms": [
"l2_ice"
],
"title": "L2 -Reminiscence- (SDVX ver.)"
},
{
"altTitles": [],
"artist": "Orbit1 & Milo",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5ec2c92bc",
"legacySongID": 2469,
"searchTerms": [
"antimatter_orbit1milo"
],
"title": "Anti-Matter"
},
{
"altTitles": [],
"artist": "Capchii feat. 初音ミク",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5f07f7ce8",
"legacySongID": 2478,
"searchTerms": [
"pickupme_capchii"
],
"title": "Pick up, Me!"
},
{
"altTitles": [],
"artist": "Ice",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5fccbe00b",
"legacySongID": 2458,
"searchTerms": [
"l_ice"
],
"title": "L (SDVX ver.)"
},
{
"altTitles": [],
"artist": "MIMI",
"data": {
"displayVersion": "nabla"
},
"id": "S19e706756c5fea133b5",
"legacySongID": 2462,
"searchTerms": [
"science_mimi"
],
"title": "サイエンス"
}
]
+32 -1
View File
@@ -6649,7 +6649,7 @@
"title": "Pop'n Music Unilab Levels"
},
{
"default": true,
"default": false,
"description": "Levels for SOUND VOLTEX in EXCEED GEAR.",
"folders": [
"1-exceed",
@@ -6741,6 +6741,37 @@
"legacyTableID": "sdvx-Single-konaste-levels",
"title": "SOUND VOLTEX (Konaste)"
},
{
"default": true,
"description": "Levels for SOUND VOLTEX in NABLA.",
"folders": [
"1-nabla",
"2-nabla",
"3-nabla",
"4-nabla",
"5-nabla",
"6-nabla",
"7-nabla",
"8-nabla",
"9-nabla",
"10-nabla",
"11-nabla",
"12-nabla",
"13-nabla",
"14-nabla",
"15-nabla",
"16-nabla",
"17-nabla",
"18-nabla",
"19-nabla",
"20-nabla"
],
"game": "sdvx",
"id": "T19e65f7d66c54197d1a",
"inactive": false,
"legacyTableID": "sdvx-Single-nabla-levels",
"title": "SOUND VOLTEX (NABLA)"
},
{
"default": false,
"description": "Levels for SOUND VOLTEX in VIVID WAVE.",
@@ -0,0 +1,151 @@
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import { UserContext } from "#context/UserContext";
import { IsNullish } from "#util/misc";
import React, { useContext } from "react";
import { Volforce } from "rg-stats";
import {
type ChartDocument,
GetSpecificGameConfig,
type PBScoreDocument,
type ScoreDocument,
} from "tachi-common";
type VF7Game = "sdvx" | "usc-controller" | "usc-keyboard";
const SHORT_LAMPS = {
CLEAR: "CLR",
"EXCESSIVE CLEAR": "EXC",
"MAXXIVE CLEAR": "MXV",
"ULTIMATE CHAIN": "UC",
} as const;
export default function VF7Cell({
score,
chart,
}: {
chart: ChartDocument<VF7Game>;
score: PBScoreDocument<VF7Game> | ScoreDocument<VF7Game>;
}) {
const { user } = useContext(UserContext);
const { settings } = useLUGPTSettings<VF7Game>();
if (IsNullish(score.calculatedData.VF7)) {
return <td>N/A</td>;
}
const vf6Target = settings?.preferences.gameSpecific.vf6Target;
const game = score.game as VF7Game;
const gameConfig = GetSpecificGameConfig(game);
const targets: Record<string, number | null> = {};
if (vf6Target && score.userID === user?.id) {
for (const lamp of ["CLEAR", "EXCESSIVE CLEAR", "ULTIMATE CHAIN"] as const) {
if (
score.scoreData.enumIndexes.lamp <=
gameConfig.providedMetrics.lamp.values.indexOf(lamp)
) {
const expectedScore = InverseVF7(vf6Target, lamp, chart.levelNum);
if (expectedScore === null) {
targets[SHORT_LAMPS[lamp]] = null;
} else {
targets[SHORT_LAMPS[lamp]] = expectedScore;
break;
}
}
}
}
const maxVF = Volforce.calculateVF7(10_000_000, "PERFECT ULTIMATE CHAIN", chart.levelNum);
return (
<td>
<strong className="underline-on-hover">{score.calculatedData.VF7}</strong>
{vf6Target !== 0 && vf6Target && user?.id === score.userID && (
<>
<br />
<div>
{score.calculatedData.VF6! >= vf6Target ? (
<small className="text-success">{vf6Target}VF Target Achieved!</small>
) : vf6Target > maxVF ? (
<small className="text-body-secondary">
{vf6Target}VF Not Possible (Max {maxVF})
</small>
) : (
Object.entries(targets).map(([k, v], i) => (
<React.Fragment key={k}>
<VF7TargetCell
clearType={k}
showClearType={i !== 0 || score.scoreData.lamp === "FAILED"}
targetDelta={v === null ? null : v - score.scoreData.score}
targetScore={v}
vf6Target={vf6Target!}
/>
<br />
</React.Fragment>
))
)}
</div>
</>
)}
</td>
);
}
function InverseVF7(
vf7: number,
lamp: "CLEAR" | "EXCESSIVE CLEAR" | "MAXXIVE CLEAR" | "ULTIMATE CHAIN",
level: number,
) {
try {
return Volforce.inverseVF7(vf7, lamp, level);
} catch (err) {
return null;
}
}
function VF7TargetCell({
targetDelta,
targetScore,
vf6Target,
clearType,
showClearType,
}: {
clearType: string;
showClearType: boolean;
targetDelta: number | null;
targetScore: number | null;
vf6Target: number;
}) {
if (targetDelta === null || targetScore === null) {
return (
<small className="text-body-secondary">
{vf6Target}VF w/ {clearType}: Not Possible
</small>
);
}
if (targetDelta < 0) {
return (
<small className="text-warning">
Score is {vf6Target}VF w/ {clearType}
</small>
);
}
const div = targetDelta / 1_000_000;
const fmt = `${div.toFixed(3)}m`;
return (
<small className="text-danger">
{vf6Target}VF{showClearType ? ` w/ ${clearType}` : ""}: +{fmt} (
{(targetScore / 1_000_000).toFixed(3)}
m)
</small>
);
}
+4 -3
View File
@@ -2,6 +2,7 @@ import MillionsScoreCell from "#components/tables/cells/MillionsScoreCell";
import SDVXJudgementCell from "#components/tables/cells/SDVXJudgementCell";
import SDVXLampCell from "#components/tables/cells/SDVXLampCell";
import VF6Cell from "#components/tables/cells/VF6Cell";
import VF7Cell from "#components/tables/cells/VF7Cell";
import { GetEnumColour } from "#lib/game-implementations";
import { type GPTClientImplementation } from "#lib/types";
import { ChangeOpacity } from "#util/color-opacity";
@@ -72,9 +73,8 @@ const SDVXCoreCells: GPTClientImplementation<"sdvx">["scoreCoreCells"] = ({ sc }
</>
);
const SDVXRatingCell: GPTClientImplementation<SDVXLikes>["ratingCell"] = ({ sc, chart }) => (
<VF6Cell chart={chart} score={sc} />
);
const SDVXRatingCell: GPTClientImplementation<SDVXLikes>["ratingCell"] = ({ sc, chart, rating }) =>
rating === "VF7" ? <VF7Cell chart={chart} score={sc} /> : <VF6Cell chart={chart} score={sc} />;
export const SDVX_IMPL: GPTClientImplementation<"sdvx"> = {
sessionImportantScoreCount: 50,
@@ -92,6 +92,7 @@ export const SDVX_IMPL: GPTClientImplementation<"sdvx"> = {
HVN: COLOUR_SET.teal,
VVD: COLOUR_SET.pink,
XCD: COLOUR_SET.blue,
NBL: COLOUR_SET.paleGreen,
MXM: COLOUR_SET.white,
ULT: COLOUR_SET.vibrantOrange,
},
@@ -137,6 +137,10 @@ export const GAME_SDVX_CONF = {
description: "VOLFORCE as it is implemented in SDVX6.",
formatter: ToDecimalPlaces(3),
},
VF7: {
description: "VOLFORCE as it is implemented in SDVX7.",
formatter: ToDecimalPlaces(3),
},
},
sessionRatingAlgs: {
ProfileVF6: {
@@ -144,6 +148,11 @@ export const GAME_SDVX_CONF = {
"The average of your best 10 VF6s this session, multiplied to be on the same scale as profile VOLFORCE.",
formatter: ToDecimalPlaces(3),
},
ProfileVF7: {
description:
"The average of your best 10 VF7s this session, multiplied to be on the same scale as profile VOLFORCE.",
formatter: ToDecimalPlaces(3),
},
},
profileRatingAlgs: {
VF6: {
@@ -151,15 +160,20 @@ export const GAME_SDVX_CONF = {
formatter: ToDecimalPlaces(3),
associatedScoreAlgs: ["VF6"],
},
VF7: {
description: "Your best 50 VF7 values added together.",
formatter: ToDecimalPlaces(3),
associatedScoreAlgs: ["VF7"],
},
},
defaultScoreRatingAlg: "VF6",
defaultSessionRatingAlg: "ProfileVF6",
defaultProfileRatingAlg: "VF6",
defaultScoreRatingAlg: "VF7",
defaultSessionRatingAlg: "ProfileVF7",
defaultProfileRatingAlg: "VF7",
difficulties: {
type: "FIXED",
order: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "MXM", "ULT"],
order: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "NBL", "MXM", "ULT"],
formatShort: {}, // they're all already short enough
formatLong: {},
default: "EXH",
@@ -186,6 +200,7 @@ export const GAME_SDVX_CONF = {
heaven: "HEAVENLY HAVEN",
vivid: "VIVID WAVE",
exceed: "EXCEED GEAR",
nabla: "NABLA",
konaste: "Konaste",
},
@@ -9,3 +9,9 @@ exports[`VF4 Validation Tests > Should throw if score is greater than 10 million
exports[`VF4 Validation Tests > Should throw if score is negative. 1`] = `"Invalid input, Score cannot be negative. score=-1."`;
exports[`VF4 Validation Tests > Should throw if the VF4 requested as an inversion is impossible. 1`] = `"Invalid input, A VF4 of 9000 is not possible on a chart with level 20. vf4=9000, level=20."`;
exports[`VF7 Validation Tests > Should throw if score is greater than 10 million. 1`] = `"Invalid input, Score cannot be greater than 10million. score=10000001."`;
exports[`VF7 Validation Tests > Should throw if score is negative. 1`] = `"Invalid input, Score cannot be negative. score=-1."`;
exports[`VF7 Validation Tests > Should throw if the volforce is impossible to achieve with this lamp/level. 1`] = `"Invalid input, A VF7 of 900 is not possible on a chart with level 1. vf7=900, level=1."`;
@@ -5,9 +5,11 @@ import {
calculateVF4,
calculateVF5,
calculateVF6,
calculateVF7,
inverseVF4,
inverseVF5,
inverseVF6,
inverseVF7,
SDVXLamps,
} from "./volforce";
@@ -818,3 +820,107 @@ test("InverseVF6 Tests", () => {
"Should throw if the volforce is impossible to achieve with this lamp/level.",
);
});
test("VF7 Tests", () => {
function VF7TestCase(
level: number,
score: number,
lamp: SDVXLamps,
expectedVF7: number,
): TestCase {
return () => {
const vf7 = calculateVF7(score, lamp, level);
expect(
vf7,
`${score} on a level ${level} chart with lamp ${lamp} should be worth ${expectedVF7} VF7. (Got ${vf7})`,
).toBe(expectedVF7);
};
}
const VF5ExampleData = [
// these should be exactly the same as vf6 as long as integer levels
// and no UCs are used
VF7TestCase(20, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.462),
VF7TestCase(19, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.438),
VF7TestCase(18, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.415),
VF7TestCase(17, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.392),
VF7TestCase(16, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.369),
VF7TestCase(15, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.346),
VF7TestCase(14, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.323),
VF7TestCase(13, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.3),
VF7TestCase(12, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.277),
VF7TestCase(11, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.254),
VF7TestCase(10, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.231),
VF7TestCase(9, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.207),
VF7TestCase(8, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.184),
VF7TestCase(7, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.161),
VF7TestCase(6, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.138),
VF7TestCase(5, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.115),
VF7TestCase(4, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.092),
VF7TestCase(3, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.069),
VF7TestCase(2, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.046),
VF7TestCase(1, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.023),
VF7TestCase(19, 9_500_000, "EXCESSIVE CLEAR", 0.357),
VF7TestCase(18, 9_923_042, "MAXXIVE CLEAR", 0.39),
VF7TestCase(19, 9_500_000, "CLEAR", 0.35),
VF7TestCase(19, 9_500_000, "FAILED", 0.175),
VF7TestCase(20, 5_000_000, "FAILED", 0.08),
VF7TestCase(20, 7_000_000, "FAILED", 0.114),
VF7TestCase(20, 8_000_000, "FAILED", 0.136),
VF7TestCase(20, 8_700_000, "FAILED", 0.153),
// decimals and UCs
VF7TestCase(19.8, 10_000_000, "PERFECT ULTIMATE CHAIN", 0.457),
VF7TestCase(18.5, 9_700_000, "ULTIMATE CHAIN", 0.38),
VF7TestCase(19.3, 9_500_000, "MAXXIVE CLEAR", 0.369),
VF7TestCase(17.5, 9_700_000, "EXCESSIVE CLEAR", 0.346),
VF7TestCase(19.6, 9_900_000, "PERFECT ULTIMATE CHAIN", 0.448),
VF7TestCase(18.7, 9_700_000, "ULTIMATE CHAIN", 0.384),
VF7TestCase(19.6, 8_000_000, "FAILED", 0.133),
];
for (const testCase of VF5ExampleData) {
testCase();
}
});
test("InverseVF7 Tests", () => {
function InvVF7TestCase(
level: number,
vf7: number,
lamp: Exclude<SDVXLamps, "PERFECT ULTIMATE CHAIN">,
expectedScore: number,
): TestCase {
return () => {
const score = inverseVF7(vf7, lamp, level);
expect(
score,
`${vf7} VF7 on a level ${level} chart with lamp ${lamp} should be inverted to ${expectedScore}. (Got ${score})`,
).toBe(expectedScore);
};
}
const testCases = [
InvVF7TestCase(20, 0.413, "CLEAR", 9_900_000),
InvVF7TestCase(20, 0.232, "MAXXIVE CLEAR", 6_971_154),
InvVF7TestCase(20, 0.421, "EXCESSIVE CLEAR", 9_900_000),
InvVF7TestCase(13, 0.121, "CLEAR", 5_817_308),
InvVF7TestCase(13, 0.121, "FAILED", 9_595_559),
InvVF7TestCase(17.5, 0.371, "EXCESSIVE CLEAR", 9_900_000),
InvVF7TestCase(19.8, 0.384, "CLEAR", 9_700_000),
InvVF7TestCase(18.5, 0.354, "MAXXIVE CLEAR", 9_500_000),
InvVF7TestCase(17.5, 0.328, "EXCESSIVE CLEAR", 9_500_000),
InvVF7TestCase(19.8, 0.39, "MAXXIVE CLEAR", 9_700_000),
InvVF7TestCase(18.7, 0.36, "CLEAR", 9_700_000),
InvVF7TestCase(17.5, 0.328, "EXCESSIVE CLEAR", 9_500_000),
InvVF7TestCase(19.8, 0.39, "MAXXIVE CLEAR", 9_700_000),
InvVF7TestCase(18.7, 0.36, "CLEAR", 9_700_000),
];
for (const testCase of testCases) {
testCase();
}
});
+95 -1
View File
@@ -48,6 +48,12 @@ const VF5LampCoefficients: Record<SDVXLamps, number> = {
FAILED: 50,
};
// NABLA buffed UC from 105 to 106, everything else matches VF6
const VF7LampCoefficients: Record<SDVXLamps, number> = {
...VF5LampCoefficients,
"ULTIMATE CHAIN": 106,
};
/**
* Calculate VOLFORCE as it's defined in SDVX4.
*
@@ -134,9 +140,13 @@ export function inverseVF5(
* @param score - The user's score. Between 0 and 10million.
* @param level - The level of the chart. Between 0 and 20, but this is not enforced.
*/
export function calculateVF6(score: integer, lamp: SDVXLamps, level: integer) {
export function calculateVF6(score: integer, lamp: SDVXLamps, level: number) {
AssertProvidedScore(score);
// levels can have decimals starting at nabla,
// flooring it to get vf6 algorithm
level = Math.floor(level);
const unroundedVF5 = CalculateWholeVF5(score, lamp, level);
// VF6 is just unroundedVF5 to 3 decimal places instead of 2.
@@ -176,6 +186,53 @@ export function inverseVF6(
return score!;
}
/**
* Calculate VOLFORCE as it's defined in SDVX7.
*
* @param score - The user's score. Between 0 and 10million.
* @param level - The level of the chart. Between 0 and 20, but this is not enforced.
*/
export function calculateVF7(score: integer, lamp: SDVXLamps, level: number) {
AssertProvidedScore(score);
const vf7 = CalculateWholeVF7(score, lamp, level);
// just like VF6, VF7 is unroundedVF5 rounded to 3 decimal places instead of 2.
return Math.floor(vf7 / 1000) / 1000;
}
/**
* Given a VF7 value and a chart level, return what score is needed to get that
* VF5.
*
* If the score needed is greater than 10million, this function will throw.
**
* @param vf7 - The VF7 to invert.
* @param lamp - The lamp for this score. This is necessary to know, as lampCoefficient
* plays a part in VF6. Passing "PERFECT ULTIMATE CHAIN" as a lamp is invalid, as inverting
* it into a score makes no sense.
* @param level - The level of the chart you're inverting for.
*/
export function inverseVF7(
vf7: number,
// Exclude PUC as input. It doesn't make sense as input, since the answer would
// always be 10million.
lamp: Exclude<SDVXLamps, "PERFECT ULTIMATE CHAIN">,
level: integer,
) {
// note: this function is actually identical to inverseVF5, but with the caveat
// that the error message is different.
const score = InvertUnroundedVF7(vf7, lamp, level);
ThrowIf(score === null, `A VF7 of ${vf7} is not possible on a chart with level ${level}.`, {
vf7,
level,
});
// guaranteed to not be null
return score!;
}
/**
* Calculate VF5 without performing any rounding. This is useful because VF5
* is floored to 2 decimal places, wherease VF6 is floored to 3. This lets us
@@ -190,6 +247,19 @@ function CalculateWholeVF5(score: integer, lamp: SDVXLamps, level: integer) {
return level * 2 * (score / 10_000_000) * gradeCoefficient * lampCoefficient;
}
/**
* Calculate VF7 without performing any rounding. This is the same as VF6
* with the exception that the level can be a number and UCs have been buffed.
*/
function CalculateWholeVF7(score: integer, lamp: SDVXLamps, level: number) {
const grade = SDVXScoreToGrade(score);
const gradeCoefficient = VF5GradeCoefficients[grade];
const lampCoefficient = VF7LampCoefficients[lamp];
return level * 2 * (score / 10_000_000) * gradeCoefficient * lampCoefficient;
}
/**
* Attempt to invert VF5 into a score.
*
@@ -214,6 +284,30 @@ function InvertUnroundedVF5(vf5: number, lamp: SDVXLamps, level: integer) {
return score;
}
/**
* Attempt to invert VF7 into a score.
*
* @returns The score if it was possible to be achieved. Else, it returns null.
*/
function InvertUnroundedVF7(vf7: number, lamp: SDVXLamps, level: number) {
// Note: PERFECT ULTIMATE CHAIN is never passed into this function from typescript
// as the calling functions Exclude<T> it from the lamps.
// However, a JS caller may call it like this anyway, so we mayaswell throw.
ThrowIf(
lamp === "PERFECT ULTIMATE CHAIN",
"PERFECT ULTIMATE CHAIN as a lampCoefficient does not make sense for an inversion, since the answer would always be 10million.",
{ lamp },
);
const lampCoefficient = VF7LampCoefficients[lamp];
const scoreTimesGradeCoef = (1_000_000 * 10_000_000 * vf7) / (2 * level * lampCoefficient);
const score = AttemptGradeCoefficientDivide(scoreTimesGradeCoef, VF5GradeCoefficients);
return score;
}
/**
* Convert a SDVX percent to the grade it represents.
* @param score - The score to convert - between 0 and 10million.
@@ -0,0 +1,228 @@
import { Command } from "commander";
import { XMLParser } from "fast-xml-parser";
import fs from "fs";
import { decode } from "iconv-lite";
import { CreateChartID, CreateSongID } from "tachi-common";
import { ReadCollection, WriteCollection } from "../../util.js";
import { InsaneCharRebinds } from "./chars.js";
const program = new Command();
program.requiredOption("-i, --input <music_db.xml>");
program.parse(process.argv);
const options = program.opts();
const VERSION = "nabla";
type XMLText<T> = { "#text": T };
type MDBChart = { difnum: XMLText<number> };
type MDBEntry = {
info: {
artist_name: string;
ascii: string;
title_name: string;
title_yomigana: string;
version: XMLText<number>;
inf_ver?: XMLText<number>;
};
difficulty: Partial<{
novice: MDBChart;
advanced: MDBChart;
exhaust: MDBChart;
infinite: MDBChart;
maximum: MDBChart;
ultimate: MDBChart;
}>;
"@_id": string;
};
type SeedSong = {
altTitles: string[];
artist: string;
data: { displayVersion: string };
id: string;
legacySongID: number;
searchTerms: string[];
title: string;
};
type SeedChart = {
data: { inGameID: number; clearTier?: unknown; pucTier?: unknown; sTier?: unknown };
difficulty: string;
id: string;
isPrimary: boolean;
legacyChartID: string;
level: string;
levelNum: number;
songID: string;
versions: string[];
};
const blacklist = new Set([1259, 1491, 1438, 1490]);
function fixString(s: string): string {
return s
.split("")
.map((c) => InsaneCharRebinds[c] ?? c)
.join("");
}
function randomHex(bytes: number): string {
const buf = new Uint8Array(bytes);
globalThis.crypto.getRandomValues(buf);
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
}
function convertVersion(input: number): string {
const table: Record<number, string> = {
1: "booth",
2: "inf",
3: "gw",
4: "heaven",
5: "vivid",
6: "exceed",
7: "nabla",
};
const v = table[input];
if (!v) throw new Error(`Unknown version field ${input} in mdb.`);
return v;
}
function convertDiff(diff: keyof MDBEntry["difficulty"], infVer: number): string {
switch (diff) {
case "novice":
return "NOV";
case "advanced":
return "ADV";
case "exhaust":
return "EXH";
case "maximum":
return "MXM";
case "ultimate":
return "ULT";
case "infinite":
switch (infVer) {
case 2:
return "INF";
case 3:
return "GRV";
case 4:
return "HVN";
case 5:
return "VVD";
case 6:
return "XCD";
case 7:
return "NBL";
default:
throw new Error(`Unknown inf_ver ${infVer}.`);
}
}
}
const parser = new XMLParser({ ignoreAttributes: false });
const utf8 = decode(fs.readFileSync(options.input), "shift-jis");
const xml = parser.parse(utf8) as { mdb: { music: MDBEntry[] } };
const songs: SeedSong[] = ReadCollection("songs-sdvx.json");
const charts: SeedChart[] = ReadCollection("charts-sdvx.json");
const inGameIDToSongID = new Map<number, string>();
const chartByKey = new Map<string, SeedChart>();
for (const c of charts) {
inGameIDToSongID.set(c.data.inGameID, c.songID);
chartByKey.set(`${c.data.inGameID}-${c.difficulty}`, c);
}
let nextLegacySongID = songs.reduce((m, s) => Math.max(m, s.legacySongID), 0) + 1;
let newSongCount = 0;
let newChartCount = 0;
let versionTagCount = 0;
let levelRefreshCount = 0;
for (const entry of xml.mdb.music) {
const inGameID = Number(entry["@_id"]);
if (blacklist.has(inGameID)) continue;
let songID = inGameIDToSongID.get(inGameID);
if (songID === undefined) {
const fixedTitle = fixString(entry.info.title_name);
const altTitles = fixedTitle !== entry.info.title_name ? [entry.info.title_name] : [];
const song: SeedSong = {
altTitles,
artist: fixString(entry.info.artist_name),
data: { displayVersion: convertVersion(entry.info.version["#text"]) },
id: CreateSongID(),
legacySongID: nextLegacySongID++,
searchTerms: [entry.info.ascii],
title: fixedTitle,
};
songs.push(song);
inGameIDToSongID.set(inGameID, song.id);
songID = song.id;
newSongCount++;
}
for (const diff of [
"novice",
"advanced",
"exhaust",
"infinite",
"maximum",
"ultimate",
] as const) {
const md = entry.difficulty[diff];
if (!md) continue;
const infVer = entry.info.inf_ver?.["#text"];
if (diff === "infinite" && infVer === 0) continue;
if (md.difnum["#text"] === 0) continue;
const difficulty = convertDiff(diff, infVer ?? 2);
const levelNum = md.difnum["#text"] / 10;
const level = Number.isInteger(levelNum) ? levelNum.toString() : levelNum.toFixed(1);
const key = `${inGameID}-${difficulty}`;
const existing = chartByKey.get(key);
if (existing) {
if (!existing.versions.includes(VERSION)) {
existing.versions.push(VERSION);
versionTagCount++;
}
if (existing.levelNum !== levelNum) {
existing.level = level;
existing.levelNum = levelNum;
levelRefreshCount++;
}
continue;
}
const chart: SeedChart = {
data: { inGameID },
difficulty,
id: CreateChartID(),
isPrimary: true,
legacyChartID: randomHex(20),
level,
levelNum,
songID,
versions: [VERSION],
};
charts.push(chart);
chartByKey.set(key, chart);
newChartCount++;
}
}
WriteCollection("songs-sdvx.json", songs);
WriteCollection("charts-sdvx.json", charts);
console.log(`new songs: ${newSongCount}`);
console.log(`new charts: ${newChartCount}`);
console.log(`version tag added: ${versionTagCount}`);
console.log(`level refreshed: ${levelRefreshCount}`);
@@ -0,0 +1,56 @@
// these chars are nonsensically remapped in the xml
// go figure
export const InsaneCharRebinds = {
齶: "♡",
齲: "♥",
齷: "é",
齪: "♣",
曦: "à",
曩: "è",
霻: "♠",
隯: "©",
鑈: "♦",
詹: "Ú",
罇: "ê",
彜: "ū",
鬯: "ī",
瑟: "ō",
驩: "Ø",
驫: "ā",
骭: "ü",
黷: "ē",
騫: "á",
躔: "🐾",
蹙: "ℱ",
趁: "Ǣ",
壘: "É",
黻: "*",
隰: "℗",
雋: "Ǜ",
鬻: "♃",
鬥: "Ã",
鬆: "Ý",
鬮: "¡",
隍: "Ü",
頽: "ä",
龕: "€",
蹇: "₂",
釁: "🍄",
鑷: "ゔ",
盥: "⚙",
餮: "Ƶ",
蔕: "ῦ",
闃: "Ā",
饌: "²",
煢: "ø",
眄: "³",
墸: " ͟͞ ",
鹹: "Ĥ",
瀑: "À",
疉: "Ö",
讎: "ö",
鑒: "₩",
壥: "Є",
醵: "乀",
鷸: "♫",
};
@@ -16,6 +16,7 @@ import {
ReadCollection,
WriteCollection,
} from "../../util";
import { InsaneCharRebinds } from "./chars";
const program = new Command();
program.requiredOption("-i, --input <music_db.xml>");
@@ -136,63 +137,6 @@ const newCharts: Array<ChartDocument<"sdvx">> = [];
// anything we don't want to include?
const blacklist = [1259, 1491, 1438, 1490];
// these chars are nonsensically remapped in the xml
// go figure
const InsaneCharRebinds = {
齶: "♡",
齲: "♥",
齷: "é",
齪: "♣",
曦: "à",
曩: "è",
霻: "♠",
隯: "©",
鑈: "♦",
詹: "Ú",
罇: "ê",
彜: "ū",
鬯: "ī",
瑟: "ō",
驩: "Ø",
驫: "ā",
骭: "ü",
黷: "ē",
騫: "á",
躔: "🐾",
蹙: "ℱ",
趁: "Ǣ",
壘: "É",
黻: "*",
隰: "℗",
雋: "Ǜ",
鬻: "♃",
鬥: "Ã",
鬆: "Ý",
鬮: "¡",
隍: "Ü",
頽: "ä",
龕: "€",
蹇: "₂",
釁: "🍄",
鑷: "ゔ",
盥: "⚙",
餮: "Ƶ",
蔕: "ῦ",
闃: "Ā",
饌: "²",
煢: "ø",
眄: "³",
墸: " ͟͞ ",
鹹: "Ĥ",
瀑: "À",
疉: "Ö",
讎: "ö",
鑒: "₩",
壥: "Є",
醵: "乀",
鷸: "♫",
};
// apply char rebinds
function fixString(string: string): string {
return string
@@ -0,0 +1,31 @@
import { type SongDocument } from "tachi-common";
import { MutateCollection } from "../../util";
import { InsaneCharRebinds } from "./chars";
function fixString(string: string): string {
return string
.split("")
.map((e) => InsaneCharRebinds[e] ?? e)
.join("");
}
MutateCollection("songs-sdvx.json", (songs) => {
const sdvxSongs = songs as Array<SongDocument<"sdvx">>;
for (const song of sdvxSongs) {
const fixedTitle = fixString(song.title);
if (fixedTitle !== song.title) {
if (!song.altTitles.includes(song.title)) {
song.altTitles.push(song.title);
}
song.title = fixedTitle;
}
song.artist = fixString(song.artist);
}
return sdvxSongs;
});
@@ -181,16 +181,22 @@ export function VF6ToClass(vf: number): SpecificUserGameStats<"sdvx">["classes"]
export const SDVXLIKE_SCORE_CALCS: GPTScoreCalcs<SDVXLikes> = (scoreData, _derivedData, chart) => ({
VF6: Volforce.calculateVF6(scoreData.score, scoreData.lamp, chart.levelNum),
VF7: Volforce.calculateVF7(scoreData.score, scoreData.lamp, chart.levelNum),
});
export const SDVXLIKE_SESSION_CALCS: GPTSessionCalcs<SDVXLikes> = (arr) => {
const v = SessionAvgBest10For("VF6")(arr);
const v2 = SessionAvgBest10For("VF7")(arr);
return { ProfileVF6: v !== null ? v * 50 : null };
return {
ProfileVF6: v !== null ? v * 50 : null,
ProfileVF7: v2 !== null ? v2 * 50 : null,
};
};
export const SDVXLIKE_PROFILE_CALCS: GPTProfileCalcs<SDVXLikes> = async (game, userID) => ({
VF6: await ProfileSumBestN("VF6", 50)(game, userID),
VF7: await ProfileSumBestN("VF7", 50)(game, userID),
});
export const SDVXLIKE_CLASS_DERIVERS: GPTClassDerivers<SDVXLikes> = (ratings) => ({