diff --git a/db/migrations/20260830000000_arcaea_inscribed.sql b/db/migrations/20260830000000_arcaea_inscribed.sql new file mode 100644 index 000000000..f5b553521 --- /dev/null +++ b/db/migrations/20260830000000_arcaea_inscribed.sql @@ -0,0 +1,16 @@ +-- Migrate existing orphans to arcaeaInGameStrID +UPDATE orphan_score +SET data = jsonb_set(data, '{difficulty}', '"AnyBeyond"') +WHERE data->>'difficulty' = 'Beyond' +AND context->>'game' = 'arcaea'; + +UPDATE orphan_score +SET data = jsonb_set(data, '{matchType}', '"arcaeaInGameStrID"') +WHERE data->>'matchType' = 'inGameStrID' +AND context->>'game' = 'arcaea'; + +-- Recalc everything because of the +0.2 clear bonus +INSERT INTO score_rederive (chart_id) +SELECT id as chart_id +FROM chart +WHERE chart.game = 'arcaea'; \ No newline at end of file diff --git a/db/seeds/charts-arcaea.json b/db/seeds/charts-arcaea.json index 901bde44f..20c403323 100644 --- a/db/seeds/charts-arcaea.json +++ b/db/seeds/charts-arcaea.json @@ -26128,7 +26128,7 @@ "id": "C19e3cb7a00a41b97fd1", "isPrimary": true, "legacyChartID": "9097b4f3fae3885f39729e53913327e827c9561c", - "level": "7", + "level": "7+", "levelNum": 7.8, "songID": "S19e3cb7a00a99fb3fe1", "versions": [ @@ -29172,7 +29172,7 @@ "isPrimary": true, "legacyChartID": "d097a9614d9f7727177948882c5f171fd5b3f119", "level": "8", - "levelNum": 8.8, + "levelNum": 8.6, "songID": "S19e3cb7a00bcd4fe829", "versions": [ "mobile" @@ -30107,5 +30107,1195 @@ "versions": [ "mobile" ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "sucromania", + "notecount": 1216 + }, + "difficulty": "Eternal", + "id": "C1a048225d2474f88b03", + "isPrimary": true, + "legacyChartID": "f9cf1880b22941f6654ebaa7a5931e36b31bfed0", + "level": "10", + "levelNum": 10.1, + "songID": "S1a048225d2313463f74", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "sucromania", + "notecount": 951 + }, + "difficulty": "Future", + "id": "C1a048225d242364db3a", + "isPrimary": true, + "legacyChartID": "35283ef82e7128c2dc95547f872ed976387d4e09", + "level": "9", + "levelNum": 9.1, + "songID": "S1a048225d2313463f74", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "sucromania", + "notecount": 477 + }, + "difficulty": "Past", + "id": "C1a048225d2475aa6d05", + "isPrimary": true, + "legacyChartID": "1b4d9ae9cbc909c149c2cf2472267f5205837b3d", + "level": "3", + "levelNum": 3, + "songID": "S1a048225d2313463f74", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "sucromania", + "notecount": 731 + }, + "difficulty": "Present", + "id": "C1a048225d24dbd7112d", + "isPrimary": true, + "legacyChartID": "2a01dfdeca8f4a22faf493593b50b8aa8f19ba1e", + "level": "6", + "levelNum": 6.5, + "songID": "S1a048225d2313463f74", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "kamippoi", + "notecount": 1116 + }, + "difficulty": "Eternal", + "id": "C1a048225d244ba1810b", + "isPrimary": true, + "legacyChartID": "e91fa08a9ce21e25ca44e3710d136bbd3cee2d6f", + "level": "10", + "levelNum": 10, + "songID": "S1a048225d240aa071b9", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "kamippoi", + "notecount": 945 + }, + "difficulty": "Future", + "id": "C1a048225d2436550a1d", + "isPrimary": true, + "legacyChartID": "212a90bef1006676cf62cb15f517c72fe2b20fbb", + "level": "9", + "levelNum": 9.3, + "songID": "S1a048225d240aa071b9", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "kamippoi", + "notecount": 522 + }, + "difficulty": "Past", + "id": "C1a048225d24d9e0526e", + "isPrimary": true, + "legacyChartID": "cd0f7af548d1e60f163378becf32939c9ce634bf", + "level": "3", + "levelNum": 3.5, + "songID": "S1a048225d240aa071b9", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "kamippoi", + "notecount": 645 + }, + "difficulty": "Present", + "id": "C1a048225d24793557d6", + "isPrimary": true, + "legacyChartID": "15ad2971e3f4d0ddaed035b08ada260aa436ae39", + "level": "7", + "levelNum": 7, + "songID": "S1a048225d240aa071b9", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "telepathy", + "notecount": 1131 + }, + "difficulty": "Eternal", + "id": "C1a048225d247d7bdc97", + "isPrimary": true, + "legacyChartID": "04a4eb1051f74c4646f81b8dba0b9044e75027c5", + "level": "10", + "levelNum": 10.4, + "songID": "S1a048225d24215b10b2", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "telepathy", + "notecount": 880 + }, + "difficulty": "Future", + "id": "C1a048225d24f3cc7389", + "isPrimary": true, + "legacyChartID": "c837a841c3554eb5635dc78cc4c37b3021f53be4", + "level": "9", + "levelNum": 9.6, + "songID": "S1a048225d24215b10b2", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "telepathy", + "notecount": 484 + }, + "difficulty": "Past", + "id": "C1a048225d246f12b306", + "isPrimary": true, + "legacyChartID": "22ee0167cab27dda2826cb85a2d7bbce0f5d6476", + "level": "4", + "levelNum": 4.5, + "songID": "S1a048225d24215b10b2", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "telepathy", + "notecount": 766 + }, + "difficulty": "Present", + "id": "C1a048225d24a800b741", + "isPrimary": true, + "legacyChartID": "24c952a785750e71bd8b896c867e6cdba8d11946", + "level": "7+", + "levelNum": 7.8, + "songID": "S1a048225d24215b10b2", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "lavie", + "notecount": 1125 + }, + "difficulty": "Eternal", + "id": "C1a048225d24d734bde6", + "isPrimary": true, + "legacyChartID": "b79d7d5e4bcf701da6ad4adc9cbe25ebe85bb97f", + "level": "9+", + "levelNum": 9.9, + "songID": "S1a048225d24796e8484", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "lavie", + "notecount": 903 + }, + "difficulty": "Future", + "id": "C1a048225d246ee6b23b", + "isPrimary": true, + "legacyChartID": "ef29ef3b265b075816c8aebf273861741285b7cd", + "level": "8+", + "levelNum": 8.8, + "songID": "S1a048225d24796e8484", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "lavie", + "notecount": 598 + }, + "difficulty": "Past", + "id": "C1a048225d24e883d324", + "isPrimary": true, + "legacyChartID": "4068847d27baea0d7d8c3ffbaf9f92e81aec8403", + "level": "3", + "levelNum": 3.5, + "songID": "S1a048225d24796e8484", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "lavie", + "notecount": 698 + }, + "difficulty": "Present", + "id": "C1a048225d2488cddfce", + "isPrimary": true, + "legacyChartID": "85ccfee46b8ea2a19ac9d4ec49256efe62ed3f77", + "level": "6", + "levelNum": 6, + "songID": "S1a048225d24796e8484", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "override", + "notecount": 1083 + }, + "difficulty": "Eternal", + "id": "C1a048225d24f9830055", + "isPrimary": true, + "legacyChartID": "da423a412bb20c1f5aeadb10991699969388bc70", + "level": "10", + "levelNum": 10.2, + "songID": "S1a048225d24bb967053", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "override", + "notecount": 917 + }, + "difficulty": "Future", + "id": "C1a048225d2405476b65", + "isPrimary": true, + "legacyChartID": "9d488a255ce0077cbf689b50126066110a20ebc6", + "level": "9", + "levelNum": 9.5, + "songID": "S1a048225d24bb967053", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "override", + "notecount": 583 + }, + "difficulty": "Past", + "id": "C1a048225d247e87a954", + "isPrimary": true, + "legacyChartID": "1fc0c9d2d7ef7bad680474d7c0fae7f5b5130674", + "level": "4", + "levelNum": 4, + "songID": "S1a048225d24bb967053", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "override", + "notecount": 723 + }, + "difficulty": "Present", + "id": "C1a048225d2430fb2f36", + "isPrimary": true, + "legacyChartID": "233b246e4dc60d0b621cea5c40d44b487087a563", + "level": "7", + "levelNum": 7.5, + "songID": "S1a048225d24bb967053", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "aishite", + "notecount": 1039 + }, + "difficulty": "Eternal", + "id": "C1a048225d253bf7b24c", + "isPrimary": true, + "legacyChartID": "8cbef7af20485c3b0733a81d1fc1a614a4bd1e84", + "level": "10", + "levelNum": 10.5, + "songID": "S1a048225d24f53846c6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "aishite", + "notecount": 837 + }, + "difficulty": "Future", + "id": "C1a048225d25a69d04b0", + "isPrimary": true, + "legacyChartID": "f1933761a67fd878a71e3a9dbc0cbdd9d9b7e3cb", + "level": "8+", + "levelNum": 8.7, + "songID": "S1a048225d24f53846c6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "aishite", + "notecount": 676 + }, + "difficulty": "Past", + "id": "C1a048225d24f3d002bf", + "isPrimary": true, + "legacyChartID": "905d52861e3cc876497cc37f416f44a172e47350", + "level": "3", + "levelNum": 3.5, + "songID": "S1a048225d24f53846c6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.15", + "inGameStrID": "aishite", + "notecount": 799 + }, + "difficulty": "Present", + "id": "C1a048225d253ad2c95a", + "isPrimary": true, + "legacyChartID": "51da4c35c368e45b10ef963ea833baebbf64d328", + "level": "6", + "levelNum": 6.5, + "songID": "S1a048225d24f53846c6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "acidgod", + "notecount": 1379 + }, + "difficulty": "Eternal", + "id": "C1a048225d25dcc6156c", + "isPrimary": true, + "legacyChartID": "5aeedd6984e0a930f33780942bce24b85ca2ae93", + "level": "10+", + "levelNum": 10.9, + "songID": "S1a048225d253cdc0e43", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "acidgod", + "notecount": 1060 + }, + "difficulty": "Future", + "id": "C1a048225d25d101ae8b", + "isPrimary": true, + "legacyChartID": "764bd06436a6cfedafaac26aad2c1cd7bbe38ebd", + "level": "9+", + "levelNum": 9.9, + "songID": "S1a048225d253cdc0e43", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "acidgod", + "notecount": 554 + }, + "difficulty": "Past", + "id": "C1a048225d251fc7e920", + "isPrimary": true, + "legacyChartID": "455ca0043936c8b216302cb211f473b487732fac", + "level": "4", + "levelNum": 4.5, + "songID": "S1a048225d253cdc0e43", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "acidgod", + "notecount": 748 + }, + "difficulty": "Present", + "id": "C1a048225d25de0c1af8", + "isPrimary": true, + "legacyChartID": "f375e9e5c950b235fd62a7e892f004b241f41a12", + "level": "7+", + "levelNum": 7.8, + "songID": "S1a048225d253cdc0e43", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "synthesis", + "notecount": 1469 + }, + "difficulty": "Eternal", + "id": "C1a048225d25fc0b98da", + "isPrimary": true, + "legacyChartID": "3e184474d6fc351b6080933070d265fcd0e649b7", + "level": "10", + "levelNum": 10.5, + "songID": "S1a048225d2548f19015", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "synthesis", + "notecount": 1192 + }, + "difficulty": "Future", + "id": "C1a048225d25ce016e61", + "isPrimary": true, + "legacyChartID": "e4e3e55673f06ec4dfac69e8c6e1192addd81cb3", + "level": "9+", + "levelNum": 9.7, + "songID": "S1a048225d2548f19015", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "synthesis", + "notecount": 764 + }, + "difficulty": "Past", + "id": "C1a048225d256e253e44", + "isPrimary": true, + "legacyChartID": "365973b299336b91c1f90e9dd616efb6a9485ccb", + "level": "4", + "levelNum": 4, + "songID": "S1a048225d2548f19015", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "synthesis", + "notecount": 1109 + }, + "difficulty": "Present", + "id": "C1a048225d2545337ddd", + "isPrimary": true, + "legacyChartID": "55c5bae24a711fc712c8b231d204d0cd5cb8389e", + "level": "8", + "levelNum": 8.2, + "songID": "S1a048225d2548f19015", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "diein", + "notecount": 1474 + }, + "difficulty": "Eternal", + "id": "C1a048225d262c69e849", + "isPrimary": true, + "legacyChartID": "191147eb726accbaa4766f95ef9de739d6055f94", + "level": "11", + "levelNum": 11.1, + "songID": "S1a048225d25e2544c0b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "diein", + "notecount": 1160 + }, + "difficulty": "Future", + "id": "C1a048225d267cd365b3", + "isPrimary": true, + "legacyChartID": "970829e7ea12b736ccfe55e1364c8b28a42fbc17", + "level": "10", + "levelNum": 10.3, + "songID": "S1a048225d25e2544c0b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "diein", + "notecount": 806 + }, + "difficulty": "Past", + "id": "C1a048225d25bce5d54d", + "isPrimary": true, + "legacyChartID": "4d467a0b8f151aacf119d47352f645ea4c0e6553", + "level": "5", + "levelNum": 5, + "songID": "S1a048225d25e2544c0b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "diein", + "notecount": 957 + }, + "difficulty": "Present", + "id": "C1a048225d2556fb65c2", + "isPrimary": true, + "legacyChartID": "2d8b462cec0474db0d961ff34a6179054d823868", + "level": "8", + "levelNum": 8.3, + "songID": "S1a048225d25e2544c0b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "chronologia", + "notecount": 1468 + }, + "difficulty": "Eternal", + "id": "C1a048225d266ed83cc5", + "isPrimary": true, + "legacyChartID": "db9f65356b349157f9607c2330a46d3a95cf9009", + "level": "10+", + "levelNum": 10.8, + "songID": "S1a048225d26066ab5a6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "chronologia", + "notecount": 1230 + }, + "difficulty": "Future", + "id": "C1a048225d26f6d20a19", + "isPrimary": true, + "legacyChartID": "20b3ca8a5a5cd606160a476a057fc26425c340cb", + "level": "9+", + "levelNum": 9.8, + "songID": "S1a048225d26066ab5a6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "chronologia", + "notecount": 790 + }, + "difficulty": "Past", + "id": "C1a048225d2642e4a4e0", + "isPrimary": true, + "legacyChartID": "b5163a8bcc22ad627b0e6c39d5469bc360ecc8b8", + "level": "4", + "levelNum": 4.5, + "songID": "S1a048225d26066ab5a6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "6.16", + "inGameStrID": "chronologia", + "notecount": 1051 + }, + "difficulty": "Present", + "id": "C1a048225d26623072a6", + "isPrimary": true, + "legacyChartID": "ac6996f437203000aea1eb71e9e495a7c18d666d", + "level": "7+", + "levelNum": 7.8, + "songID": "S1a048225d26066ab5a6", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "un", + "notecount": 1823 + }, + "difficulty": "Eternal", + "id": "C1a048225d2622ac99bf", + "isPrimary": true, + "legacyChartID": "09d71b7b285caa2bb03ea125f5aa79cb0404206d", + "level": "10+", + "levelNum": 10.9, + "songID": "S1a048225d260ce5389b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "un", + "notecount": 1502 + }, + "difficulty": "Future", + "id": "C1a048225d26257cfeda", + "isPrimary": true, + "legacyChartID": "ba74e015485887880083ae3626cd228d66ed571b", + "level": "9+", + "levelNum": 9.9, + "songID": "S1a048225d260ce5389b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "un", + "notecount": 853 + }, + "difficulty": "Past", + "id": "C1a048225d2655fba368", + "isPrimary": true, + "legacyChartID": "a947f61d15435121f32f1783b4ede0c23b2b209a", + "level": "4", + "levelNum": 4.5, + "songID": "S1a048225d260ce5389b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "un", + "notecount": 1040 + }, + "difficulty": "Present", + "id": "C1a048225d26329d90e0", + "isPrimary": true, + "legacyChartID": "86b7f20018c102451b1a392e085fba69c635b1ae", + "level": "7", + "levelNum": 7.8, + "songID": "S1a048225d260ce5389b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "ember", + "notecount": 1070 + }, + "difficulty": "Future", + "id": "C1a048225d262348ba23", + "isPrimary": true, + "legacyChartID": "bea975d60bd604d4fa9b6a58581a5462af13c38c", + "level": "9+", + "levelNum": 9.9, + "songID": "S1a048225d260e77c468", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "ember", + "notecount": 740 + }, + "difficulty": "Past", + "id": "C1a048225d26a61b14f9", + "isPrimary": true, + "legacyChartID": "39efefb71e361bf4a6ac149cab6931fe1b6d78b5", + "level": "5", + "levelNum": 5, + "songID": "S1a048225d260e77c468", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "ember", + "notecount": 824 + }, + "difficulty": "Present", + "id": "C1a048225d26c4c5cba3", + "isPrimary": true, + "legacyChartID": "cb74bebe4e616162d2331af673675e28f1131bd7", + "level": "8", + "levelNum": 8.1, + "songID": "S1a048225d260e77c468", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "dreadarea", + "notecount": 1405 + }, + "difficulty": "Future", + "id": "C1a048225d267a361d39", + "isPrimary": true, + "legacyChartID": "4c0eb3885781a90afb9cec736513dc040c27de41", + "level": "10", + "levelNum": 10.3, + "songID": "S1a048225d260ef2b185", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "dreadarea", + "notecount": 1663 + }, + "difficulty": "Inscribed", + "id": "C1a048225d2644efd70a", + "isPrimary": true, + "legacyChartID": "5b7f53d1239274890b78960a0bd8a4bbde1bf52f", + "level": "11", + "levelNum": 11.4, + "songID": "S1a048225d260ef2b185", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "dreadarea", + "notecount": 858 + }, + "difficulty": "Past", + "id": "C1a048225d26fa0b3f5a", + "isPrimary": true, + "legacyChartID": "0f9e369182682681cc61264930e889dbbdf25152", + "level": "5", + "levelNum": 5.5, + "songID": "S1a048225d260ef2b185", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "dreadarea", + "notecount": 1002 + }, + "difficulty": "Present", + "id": "C1a048225d26defc7581", + "isPrimary": true, + "legacyChartID": "ee8a59785c181be02e70c25d9cad21e8c49b796d", + "level": "8+", + "levelNum": 8.3, + "songID": "S1a048225d260ef2b185", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "rivenpilgrim", + "notecount": 1503 + }, + "difficulty": "Future", + "id": "C1a048225d2691ffd781", + "isPrimary": true, + "legacyChartID": "626fa2e726055a86a9033f940d078cfbc4149d0a", + "level": "10", + "levelNum": 10.4, + "songID": "S1a048225d263a3bf06d", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "rivenpilgrim", + "notecount": 2032 + }, + "difficulty": "Inscribed", + "id": "C1a048225d26b2729cbd", + "isPrimary": true, + "legacyChartID": "517b23a6fcd02071f49b42cb7cdb7f8450f07bee", + "level": "11", + "levelNum": 11.5, + "songID": "S1a048225d263a3bf06d", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "rivenpilgrim", + "notecount": 889 + }, + "difficulty": "Past", + "id": "C1a048225d267af75452", + "isPrimary": true, + "legacyChartID": "6de42ad7c14c70ebb562b9282d3e400b8d095ad4", + "level": "5", + "levelNum": 5.5, + "songID": "S1a048225d263a3bf06d", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "rivenpilgrim", + "notecount": 1052 + }, + "difficulty": "Present", + "id": "C1a048225d26b23ab918", + "isPrimary": true, + "legacyChartID": "895bfd7f0e0cd9a8e3fd2b9b4c791af112e4e1ca", + "level": "8", + "levelNum": 8.5, + "songID": "S1a048225d263a3bf06d", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "balor", + "notecount": 1138 + }, + "difficulty": "Eternal", + "id": "C1a048225d26862d9959", + "isPrimary": true, + "legacyChartID": "c1158edb2ba318e3ff156389ea85ea9bc84c64e9", + "level": "10", + "levelNum": 10.1, + "songID": "S1a048225d263c54c554", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "balor", + "notecount": 900 + }, + "difficulty": "Future", + "id": "C1a048225d263d567064", + "isPrimary": true, + "legacyChartID": "b3d970f54437662a26f35aafb3189abffd1e0315", + "level": "9", + "levelNum": 9.5, + "songID": "S1a048225d263c54c554", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "balor", + "notecount": 677 + }, + "difficulty": "Past", + "id": "C1a048225d2668446c01", + "isPrimary": true, + "legacyChartID": "01fc856a9bacf67c3c15d1d1dc7414931a4c5010", + "level": "3", + "levelNum": 3.5, + "songID": "S1a048225d263c54c554", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "balor", + "notecount": 673 + }, + "difficulty": "Present", + "id": "C1a048225d26a412178e", + "isPrimary": true, + "legacyChartID": "c18566a4d6c4f66d2ca5a9bb30143119a3795a80", + "level": "6", + "levelNum": 7.5, + "songID": "S1a048225d263c54c554", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "deinosphainein", + "notecount": 1888 + }, + "difficulty": "Future", + "id": "C1a048225d2683e54434", + "isPrimary": true, + "legacyChartID": "0736f3d3a070239583439884f88c10edcc0ecb3f", + "level": "10+", + "levelNum": 10.9, + "songID": "S1a048225d2652d61d6b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "deinosphainein", + "notecount": 2228 + }, + "difficulty": "Inscribed", + "id": "C1a048225d2648bc87e3", + "isPrimary": true, + "legacyChartID": "10e9e275d2e313c6577fa82c38da78277a25c664", + "level": "12", + "levelNum": 12, + "songID": "S1a048225d2652d61d6b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "deinosphainein", + "notecount": 1008 + }, + "difficulty": "Past", + "id": "C1a048225d26edb62dd6", + "isPrimary": true, + "legacyChartID": "ea4b008e7d1cdb4598cc55bd53921de2784f77b1", + "level": "7", + "levelNum": 7.5, + "songID": "S1a048225d2652d61d6b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "deinosphainein", + "notecount": 1178 + }, + "difficulty": "Present", + "id": "C1a048225d26d2d48eba", + "isPrimary": true, + "legacyChartID": "23bf468d4b77836e69f9bdeef0812158b37a43a9", + "level": "9", + "levelNum": 9.6, + "songID": "S1a048225d2652d61d6b", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "cataclysmcry", + "notecount": 1614 + }, + "difficulty": "Future", + "id": "C1a048225d262fa608d7", + "isPrimary": true, + "legacyChartID": "36a19143d37eef5dede9427756544d4bdb9213b0", + "level": "10", + "levelNum": 10.6, + "songID": "S1a048225d26d1dcad1e", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "cataclysmcry", + "notecount": 2119 + }, + "difficulty": "Inscribed", + "id": "C1a048225d2654b443ae", + "isPrimary": true, + "legacyChartID": "00cfb342708f3b8d73cb5102a9e671cf6ba34c31", + "level": "11+", + "levelNum": 11.8, + "songID": "S1a048225d26d1dcad1e", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "cataclysmcry", + "notecount": 920 + }, + "difficulty": "Past", + "id": "C1a048225d26337f56c8", + "isPrimary": true, + "legacyChartID": "16b663bc4fa72b59d6a70fb6e2202e85de6da3a9", + "level": "6", + "levelNum": 6.5, + "songID": "S1a048225d26d1dcad1e", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "cataclysmcry", + "notecount": 1137 + }, + "difficulty": "Present", + "id": "C1a048225d2643e79beb", + "isPrimary": true, + "legacyChartID": "8261c38d5bf2be732706dc963333a80aa10f2ef5", + "level": "8+", + "levelNum": 8.8, + "songID": "S1a048225d26d1dcad1e", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "sacrosanct", + "notecount": 1888 + }, + "difficulty": "Future", + "id": "C1a06240e5527a97b636", + "isPrimary": true, + "legacyChartID": "14a7c99939af737f74ee26ec537925fe11862d02", + "level": "10+", + "levelNum": 10.7, + "songID": "S1a06240e55220088ac4", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "sacrosanct", + "notecount": 1256 + }, + "difficulty": "Past", + "id": "C1a06240e5522ab72e9a", + "isPrimary": true, + "legacyChartID": "afd181d9a5f0eb21cc07b69a5200b8197e666499", + "level": "6", + "levelNum": 6.5, + "songID": "S1a06240e55220088ac4", + "versions": [ + "mobile" + ] + }, + { + "data": { + "displayVersion": "7.0", + "inGameStrID": "sacrosanct", + "notecount": 1385 + }, + "difficulty": "Present", + "id": "C1a06240e5522eab15d4", + "isPrimary": true, + "legacyChartID": "b7d12a1c76119721fbd9962d64c47c9453eda432", + "level": "9", + "levelNum": 9.6, + "songID": "S1a06240e55220088ac4", + "versions": [ + "mobile" + ] } ] \ No newline at end of file diff --git a/db/seeds/songs-arcaea.json b/db/seeds/songs-arcaea.json index d63c0b865..1463b9b7c 100644 --- a/db/seeds/songs-arcaea.json +++ b/db/seeds/songs-arcaea.json @@ -8258,5 +8258,284 @@ "코스모자이랄" ], "title": "Cosmogyral" + }, + { + "altTitles": [ + "ザラメノキワミ" + ], + "artist": "STEAKA", + "data": { + "songPack": "Memory Archive" + }, + "id": "S1a048225d2313463f74", + "legacySongID": 549, + "searchTerms": [ + "zaramenokiwami", + "수크로마니아", + "자라메노키와미" + ], + "title": "Sucromania" + }, + { + "altTitles": [ + "神っぽいな" + ], + "artist": "ピノキオピー", + "data": { + "songPack": "Virtual Singers" + }, + "id": "S1a048225d240aa071b9", + "legacySongID": 551, + "searchTerms": [ + "kamippoina", + "かみっぽいな", + "신 같네", + "카밋포이나", + "카밋뽀이나", + "가디시" + ], + "title": "God-ish" + }, + { + "altTitles": [ + "テレパシ" + ], + "artist": "DECO*27", + "data": { + "songPack": "Virtual Singers" + }, + "id": "S1a048225d24215b10b2", + "legacySongID": 553, + "searchTerms": [ + "terepashi", + "텔레파시" + ], + "title": "Telepathy" + }, + { + "altTitles": [ + "ラヴィ" + ], + "artist": "すりぃ", + "data": { + "songPack": "Virtual Singers" + }, + "id": "S1a048225d24796e8484", + "legacySongID": 550, + "searchTerms": [ + "ravi", + "라비" + ], + "title": "Lavie" + }, + { + "altTitles": [ + "オーバーライド" + ], + "artist": "吉田夜世", + "data": { + "songPack": "Virtual Singers" + }, + "id": "S1a048225d24bb967053", + "legacySongID": 552, + "searchTerms": [ + "obaraido", + "오버라이드" + ], + "title": "Override" + }, + { + "altTitles": [ + "愛して愛して愛して" + ], + "artist": "きくお", + "data": { + "songPack": "Virtual Singers" + }, + "id": "S1a048225d24f53846c6", + "legacySongID": 554, + "searchTerms": [ + "aishiteaishiteaishite", + "あいしてあいしてあいして", + "사랑해줘 사랑해줘 사랑해줘", + "아이시테 아이시테 아이시테", + "러브 미 러브 미 러브 미" + ], + "title": "Love me, Love me, Love me" + }, + { + "altTitles": [], + "artist": "Yuta Imai", + "data": { + "songPack": "Memory Archive" + }, + "id": "S1a048225d253cdc0e43", + "legacySongID": 556, + "searchTerms": [ + "あしっどごっど", + "애시드 갓" + ], + "title": "Acid God" + }, + { + "altTitles": [], + "artist": "tn-shi", + "data": { + "songPack": "Memory Archive" + }, + "id": "S1a048225d2548f19015", + "legacySongID": 555, + "searchTerms": [ + "しんせしす", + "신테시스", + "신세시스" + ], + "title": "Synthesis." + }, + { + "altTitles": [], + "artist": "TAK x Sobrem", + "data": { + "songPack": "Memory Archive" + }, + "id": "S1a048225d25e2544c0b", + "legacySongID": 557, + "searchTerms": [ + "だいいん", + "だいん", + "다이 인" + ], + "title": "DIE IN" + }, + { + "altTitles": [], + "artist": "lowiro Sound Team (ak+q x nitro)", + "data": { + "songPack": "Memory Archive" + }, + "id": "S1a048225d26066ab5a6", + "legacySongID": 558, + "searchTerms": [ + "くろのろじあ", + "크로노로지아" + ], + "title": "chronologia" + }, + { + "altTitles": [], + "artist": "黒魔", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d260ce5389b", + "legacySongID": 561, + "searchTerms": [ + "あん", + "언", + "은", + "운" + ], + "title": "un" + }, + { + "altTitles": [], + "artist": "Powerless & siqlo feat. Tia", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d260e77c468", + "legacySongID": 565, + "searchTerms": [ + "えんばー", + "엠버" + ], + "title": "Ember" + }, + { + "altTitles": [], + "artist": "Ashrount vs. 打打だいず", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d260ef2b185", + "legacySongID": 560, + "searchTerms": [ + "どれっどえりあ", + "드레드 에어리어" + ], + "title": "DREAD AREA" + }, + { + "altTitles": [], + "artist": "rejection & Supire feat. eili", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d263a3bf06d", + "legacySongID": 562, + "searchTerms": [ + "りべんぴるぐりめーじ", + "리벤 필그러메이지" + ], + "title": "Riven Pilgrimage" + }, + { + "altTitles": [], + "artist": "Redsign", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d263c54c554", + "legacySongID": 559, + "searchTerms": [ + "ばろーる", + "ばろる", + "발러", + "밸러" + ], + "title": "Balor" + }, + { + "altTitles": [], + "artist": "Blacklolita vs Juggernaut.", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d2652d61d6b", + "legacySongID": 564, + "searchTerms": [ + "えいのすふぁいねいん", + "에이노스 파이네인" + ], + "title": "DEINOS PHAINEIN" + }, + { + "altTitles": [], + "artist": "Aid", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a048225d26d1dcad1e", + "legacySongID": 563, + "searchTerms": [ + "かたくりずむくらい", + "카타클리즘 시티" + ], + "title": "Cataclysm Cry" + }, + { + "altTitles": [], + "artist": "Silentroom Dismantled XOR かたぎり", + "data": { + "songPack": "Divine Oblivion" + }, + "id": "S1a06240e55220088ac4", + "legacySongID": 566, + "searchTerms": [ + "sacrosanct", + "さくろさんくと", + "사크로상트" + ], + "title": "⊥⊬" } ] \ No newline at end of file diff --git a/docs/src/codebase/batch-manual/index.md b/docs/src/codebase/batch-manual/index.md index d4bf75000..0b0c79e1b 100644 --- a/docs/src/codebase/batch-manual/index.md +++ b/docs/src/codebase/batch-manual/index.md @@ -168,6 +168,17 @@ means that it will check for any of `INF/GRV/HVN/VVD/XCD` for this song. This is useful for services that store all of those as the same difficulty. +- arcaeaInGameStrID + +This uses the in-game-ID for this Arcaea song. You must specify +the difficulty for this chart as well. + +The reason Arcaea gets its own special `matchType` is because this +matchType supports `difficulty: "AnyBeyond"`. This special difficulty +means that it will check for any of `Beyond/Inscribed` for this song. + +This is useful for services that store all of those as the same difficulty. + - ddrSongHash This is a 32-character hash used by the official DDR e-amusement website to diff --git a/docs/src/game-support/common-config/match-types.md b/docs/src/game-support/common-config/match-types.md index ecc820e7a..5bdda4975 100644 --- a/docs/src/game-support/common-config/match-types.md +++ b/docs/src/game-support/common-config/match-types.md @@ -45,6 +45,12 @@ Looks up on the in game ID for this chart, but allows a special difficulty strin If `"ANY_INF"` is the difficulty, then that difficulty will try to find a chart with this in game ID where the difficulty is any of INF, GRV, HVN, VVD or XCD. +### `arcaeaInGameStrID` + +Looks up on the in game ID for this chart, but allows a special difficulty string - `"AnyBeyond"` to be passed. + +If `"AnyBeyond"` is the difficulty, then that difficulty will try to find a chart with this in game ID where the difficulty is any of Beyond or Inscribed. + ### `songTitle` Looks up the song on its title or any of its defined `altTitles`. Can also match the artist field of the song if an `artist` property is defined. diff --git a/docs/src/game-support/games/arcaea-Touch.md b/docs/src/game-support/games/arcaea-Touch.md index 6b9832d8e..0bb2d51ae 100644 --- a/docs/src/game-support/games/arcaea-Touch.md +++ b/docs/src/game-support/games/arcaea-Touch.md @@ -50,21 +50,24 @@ The following judgements are defined: | Name | Description | | :: | :: | -| `naivePotential` | The average of your best 10 potentials this session. | +| `naivePotential` | The average of your best 10 Potentials this session. | ### Profile Rating Algorithms | Name | Description | | :: | :: | -| `naivePotential` | The average of your best 30 potential values. This is different to in-game, as it does not take into account your recent scores in any way. | +| `naivePotential` | The average of your best 50 Potential values, with the best 10 being counted twice. Named naivePotential for legacy reasons, but it's actually Potential as implemented in Arcaea. | ## Difficulties +`Beyond` and `Inscribed` can use the alias `AnyBeyond` for imports using `arcaeaInGameStrID`. + - `Past` - `Present` - `Future` - `Eternal` - `Beyond` +- `Inscribed` ## Classes @@ -82,6 +85,6 @@ The following judgements are defined: ## Supported Match Types -- `inGameStrID` +- `arcaeaInGameStrID` - `songTitle` - `tachiSongID` diff --git a/typescript/client/src/app/pages/dashboard/import/BatchManualPage.tsx b/typescript/client/src/app/pages/dashboard/import/BatchManualPage.tsx index bfeb092a8..d6e64dd6d 100644 --- a/typescript/client/src/app/pages/dashboard/import/BatchManualPage.tsx +++ b/typescript/client/src/app/pages/dashboard/import/BatchManualPage.tsx @@ -64,6 +64,10 @@ export default function BatchManualPage() { const game = ResolveBatchManualGameFromMeta(data.meta); + if (!("service" in data.meta)) { + throw new Error("Invalid BATCH-MANUAL: Missing service name"); + } + const err = p(data, PR_BATCH_MANUAL(game)); if (err) { diff --git a/typescript/client/src/lib/games/arcaea.tsx b/typescript/client/src/lib/games/arcaea.tsx index 0d6e64e08..0049c928d 100644 --- a/typescript/client/src/lib/games/arcaea.tsx +++ b/typescript/client/src/lib/games/arcaea.tsx @@ -22,6 +22,7 @@ const ARCAEA_DIFFICULTY_COLORS: GameClientImplementation< Future: COLOUR_SET.purple, Eternal: COLOUR_SET.paleBlue, Beyond: COLOUR_SET.vibrantRed, + Inscribed: COLOUR_SET.vibrantBlue, }; const ARCAEA_ENUM_COLORS: GameClientImplementation["enumColours"] = { @@ -162,6 +163,11 @@ export const ARCAEA_TOUCH_IMPL: GameClientImplementation<"arcaea"> = { grade: "sort-alpha-up", lamp: "lightbulb", }, + ratingAlgNameOverrides: { + profile: { + naivePotential: "Potential", + }, + }, enumColours: ARCAEA_ENUM_COLORS, classColours: ARCAEA_COLORS, difficultyColours: ARCAEA_DIFFICULTY_COLORS, diff --git a/typescript/client/src/util/db-converters/arcaea.ts b/typescript/client/src/util/db-converters/arcaea.ts index 5d634e958..7b275cfbb 100644 --- a/typescript/client/src/util/db-converters/arcaea.ts +++ b/typescript/client/src/util/db-converters/arcaea.ts @@ -23,7 +23,7 @@ const getDifficulty = (score: ArcaeaST3ScoreRow) => { case 2: return "Future"; case 3: - return "Beyond"; + return "AnyBeyond"; case 4: return "Eternal"; default: @@ -100,7 +100,7 @@ export function convertArcaeaDB(db: Database): { result: ArcaeaBatchManual; warn } scores.push({ identifier: row.songId, - matchType: "inGameStrID", + matchType: "arcaeaInGameStrID", difficulty: getDifficulty(row), score: row.score, lamp: getLamp(row), diff --git a/typescript/common/src/config/game-support/arcaea.ts b/typescript/common/src/config/game-support/arcaea.ts index adc5a80ab..41e06c1e2 100644 --- a/typescript/common/src/config/game-support/arcaea.ts +++ b/typescript/common/src/config/game-support/arcaea.ts @@ -82,21 +82,21 @@ export const GAME_ARCAEA_CONF = { scoreRatingAlgs: { potential: { description: "Potential as it is implemented in Arcaea.", - formatter: ToDecimalPlaces(2), + formatter: ToDecimalPlaces(3), canSetGoalsOn: true, }, }, sessionRatingAlgs: { naivePotential: { - description: "The average of your best 10 potentials this session.", - formatter: ToDecimalPlaces(2), + description: "The average of your best 10 Potentials this session.", + formatter: ToDecimalPlaces(3), }, }, profileRatingAlgs: { naivePotential: { description: - "The average of your best 30 Potential values. This is different to the in-game algorithm, as it does not take your recent scores into account in any way.", - formatter: (v) => (Math.round(v * 100.0) / 100.0).toFixed(2), + "The average of your best 50 Potential values, with the best 10 being counted twice. Profile Potential as it's implemented in Arcaea 7.0+.", + formatter: (v) => (Math.round(v * 1000.0) / 1000.0).toFixed(3), associatedScoreAlgs: ["potential"], }, }, @@ -107,13 +107,14 @@ export const GAME_ARCAEA_CONF = { difficulties: { type: "FIXED", - order: ["Past", "Present", "Future", "Eternal", "Beyond"], + order: ["Past", "Present", "Future", "Eternal", "Beyond", "Inscribed"], formatShort: { Past: "PST", Present: "PRS", Future: "FTR", Eternal: "ETR", Beyond: "BYD", + Inscribed: "INS", }, formatLong: {}, default: "Future", @@ -147,5 +148,5 @@ export const GAME_ARCAEA_CONF = { preferences: z.strictObject({}), scoreMeta: z.strictObject({}), - supportedMatchTypes: ["inGameStrID", "songTitle", "tachiSongID"], + supportedMatchTypes: ["arcaeaInGameStrID", "songTitle", "tachiSongID"], } as const satisfies INTERNAL_GAME_CONFIG; diff --git a/typescript/common/src/lib/schemas.ts b/typescript/common/src/lib/schemas.ts index cd39b6b50..161f88154 100644 --- a/typescript/common/src/lib/schemas.ts +++ b/typescript/common/src/lib/schemas.ts @@ -39,6 +39,7 @@ const PR_BATCH_MANUAL_SCORE = (game: V3Game): PrudenceSchema => { "uscChartHash", "popnChartHash", "ddrSongHash", + "arcaeaInGameStrID", ), identifier: "string", comment: optNull(p.isBoundedString(3, 240)), diff --git a/typescript/common/src/types/batch-manual.ts b/typescript/common/src/types/batch-manual.ts index a5bc40775..3962cc61d 100644 --- a/typescript/common/src/types/batch-manual.ts +++ b/typescript/common/src/types/batch-manual.ts @@ -21,6 +21,7 @@ type MatchTypesNoDifficulty = // These MatchTypes need `difficulty` set in the batch manual. type MatchTypesWithDifficulty = + | "arcaeaInGameStrID" | "ddrSongHash" | "inGameID" | "inGameStrID" diff --git a/typescript/rg-stats/src/algorithms/potential.test.ts b/typescript/rg-stats/src/algorithms/potential.test.ts index b80dd839c..0ffbc49a9 100644 --- a/typescript/rg-stats/src/algorithms/potential.test.ts +++ b/typescript/rg-stats/src/algorithms/potential.test.ts @@ -1,16 +1,27 @@ import { expect, test } from "vitest"; import { isAprx } from "../test-utils/approx"; import { TestCase } from "../test-utils/test-case"; -import { calculate } from "./potential"; +import { ArcaeaLamp, calculate } from "./potential"; test("Arcaea Potential Tests", () => { function MakeTestCase(score: number, level: number, expectedPotential: number): TestCase { - return () => + return () => { + const clearLamps: ArcaeaLamp[] = ["EASY CLEAR", "CLEAR", "HARD CLEAR", "FULL RECALL"]; isAprx( - calculate(score, level), + calculate(score, level, "LOST"), expectedPotential, - `A score of ${score} on a chart of level ${level} should be worth ${expectedPotential}`, + `A LOST score of ${score} on a chart of level ${level} should be worth roughly ${expectedPotential}`, + 2, ); + clearLamps.forEach((lamp) => { + isAprx( + calculate(score, level, lamp), + expectedPotential + 0.2, + `A ${lamp} score of ${score} on a chart of level ${level} should be worth roughly ${expectedPotential + 0.2}`, + 2, + ); + }); + }; } const testCases = [ @@ -19,7 +30,6 @@ test("Arcaea Potential Tests", () => { MakeTestCase(9_932_746, 10.9, 12.56), MakeTestCase(9_805_015, 11.6, 12.63), - MakeTestCase(10_000_000, 7.0, 9.0), MakeTestCase(9_900_000, 8.0, 9.5), MakeTestCase(9_800_000, 9.5, 10.5), MakeTestCase(9_500_000, 8.9, 8.9), @@ -32,8 +42,22 @@ test("Arcaea Potential Tests", () => { testCase(); } + isAprx( + calculate(10_000_000, 7.0, "PURE MEMORY"), + 9.2, + `A PURE MEMORY on a chart of level 7.0 should be worth 9.2`, + 3, + ); + + expect(() => calculate(9_999_999, 7.0, "PURE MEMORY")).toThrow( + /.*PURE MEMORY cannot be below 10 million.*/u, + ); + expect(() => calculate(10_000_000, 7.0, "CLEAR")).toThrow( + /.*Scores exceeding 10 million must be PURE MEMORY.*/u, + ); + expect( - calculate(10_000_000, 10.0), + calculate(10_000_000, 10.0, "PURE MEMORY"), "Anything above 10,000,000 should give identical potential.", - ).toBe(calculate(10_001_000, 10.0)); + ).toBe(calculate(10_001_000, 10.0, "PURE MEMORY")); }); diff --git a/typescript/rg-stats/src/algorithms/potential.ts b/typescript/rg-stats/src/algorithms/potential.ts index 43d41d6c7..f4345ac07 100644 --- a/typescript/rg-stats/src/algorithms/potential.ts +++ b/typescript/rg-stats/src/algorithms/potential.ts @@ -1,16 +1,35 @@ import { ThrowIf } from "../util/throw-if"; +export type ArcaeaLamp = + | "LOST" + | "CLEAR" + | "EASY CLEAR" + | "HARD CLEAR" + | "FULL RECALL" + | "PURE MEMORY"; + /** * Calculate Arcaea potential for a score. * * @param score - The score to calculate the potential for. * @param internalChartLevel - The internal decimal level of the chart the score was achieved on. + * @param lamp - The lamp of this score. As of Arcaea 7.0, this determines the +0.2 clear bonus. */ -export function calculate(score: number, internalChartLevel: number) { +export function calculate(score: number, internalChartLevel: number, lamp: ArcaeaLamp) { ThrowIf.negative(score, "Score cannot be negative.", { score }); ThrowIf.negative(internalChartLevel, "Internal chart level cannot be negative.", { level: internalChartLevel, }); + ThrowIf( + lamp === "PURE MEMORY" && score < 10_000_000, + "PURE MEMORY cannot be below 10 million.", + { score, lamp }, + ); + ThrowIf( + lamp !== "PURE MEMORY" && score >= 10_000_000, + "Scores exceeding 10 million must be PURE MEMORY.", + { score, lamp }, + ); let potential = 0; @@ -22,5 +41,9 @@ export function calculate(score: number, internalChartLevel: number) { potential = internalChartLevel + (score - 9_500_000) / 300_000; } + if (lamp !== "LOST") { + potential += 0.2; + } + return Math.max(potential, 0); } diff --git a/typescript/seeds-scripts/rerunners/arcaea/merge-songlist.ts b/typescript/seeds-scripts/rerunners/arcaea/merge-songlist.ts index bcc0db715..890d9dbda 100644 --- a/typescript/seeds-scripts/rerunners/arcaea/merge-songlist.ts +++ b/typescript/seeds-scripts/rerunners/arcaea/merge-songlist.ts @@ -22,6 +22,7 @@ type LocalizedSearchTerms = Record>; interface SonglistChart { ratingClass: integer; + ratingClassAlias?: integer; title_localized?: LocalizedText; artist?: string; audioOverride?: boolean; @@ -93,7 +94,7 @@ class MultiMapUniqueValues { } } -function convertDifficulty(input: integer): Difficulties["arcaea"] { +function convertDifficulty(input: integer, alias?: integer): Difficulties["arcaea"] { switch (input) { case 0: return "Past"; @@ -102,7 +103,11 @@ function convertDifficulty(input: integer): Difficulties["arcaea"] { case 2: return "Future"; case 3: - return "Beyond"; + if (alias === 1) { + return "Inscribed"; + } else { + return "Beyond"; + } case 4: return "Eternal"; } @@ -227,7 +232,7 @@ for (const entry of data.songs) { continue; } - const difficulty = convertDifficulty(chart.ratingClass); + const difficulty = convertDifficulty(chart.ratingClass, chart.ratingClassAlias); const exists = existingCharts.get(`${inGameID}-${difficulty}`); if (exists) { diff --git a/typescript/seeds-scripts/rerunners/arcaea/notes-and-constants.ts b/typescript/seeds-scripts/rerunners/arcaea/notes-and-constants.ts index 527d1ddee..ba9c9a150 100644 --- a/typescript/seeds-scripts/rerunners/arcaea/notes-and-constants.ts +++ b/typescript/seeds-scripts/rerunners/arcaea/notes-and-constants.ts @@ -29,6 +29,7 @@ const inverseConvertDifficulty = (input: string) => { case "Future": return 2; case "Beyond": + case "Inscribed": return 3; case "Eternal": return 4; diff --git a/typescript/seeds-scripts/test/match-type.test.ts b/typescript/seeds-scripts/test/match-type.test.ts index a1a275220..21058ccc5 100644 --- a/typescript/seeds-scripts/test/match-type.test.ts +++ b/typescript/seeds-scripts/test/match-type.test.ts @@ -68,6 +68,18 @@ const MATCH_TYPE_CHECKS: Record< return `${c.data.inGameID}-${diff}`; }, }, + arcaeaInGameStrID: { + type: "CHARTS", + fn: (c) => { + let diff = c.difficulty; + + if (["Beyond", "Inscribed"].includes(diff)) { + diff = "AnyBeyond"; + } + + return `${c.data.inGameStrID}-${diff}`; + }, + }, uscChartHash: { type: "CHARTS", fn: (c) => c.data.hashSHA1 }, ddrSongHash: { type: "SONGS", @@ -126,7 +138,7 @@ for (const { game, matchType } of uniquenessChecks) { } if ( - matchType === "inGameStrID" && + (matchType === "inGameStrID" || matchType === "arcaeaInGameStrID") && el.data.inGameStrID === null && el.data.inGameStrID === undefined ) { diff --git a/typescript/server/src/game-implementations/games/arcaea.test.ts b/typescript/server/src/game-implementations/games/arcaea.test.ts index 42300915a..04ce9012d 100644 --- a/typescript/server/src/game-implementations/games/arcaea.test.ts +++ b/typescript/server/src/game-implementations/games/arcaea.test.ts @@ -37,7 +37,7 @@ const baseMetrics: MongoProvidedMetrics["arcaea"] = { }; const scoreData: ScoreData<"arcaea"> = { - lamp: "CLEAR", + lamp: "LOST", score: 9_979_366, grade: "EX+", judgements: { @@ -48,7 +48,7 @@ const scoreData: ScoreData<"arcaea"> = { optional: { enumIndexes: {} }, enumIndexes: { grade: ARCAEA_GRADES.EX_PLUS, - lamp: ARCAEA_LAMPS.CLEAR, + lamp: ARCAEA_LAMPS.LOST, }, }; @@ -136,7 +136,7 @@ describe("ARCAEA_IMPL", () => { expect( ARCAEA_IMPL.scoreCalcs(scoreData, ARCAEA_IMPL.scoreDeriver(scoreData, chart), chart) .potential, - ).toBe(11.99683); + ).toBeCloseTo(11.99683, 3); }); describe("classDerivers (naivePotential → badge)", () => { @@ -226,6 +226,8 @@ describe("ARCAEA_IMPL", () => { const pb = await CreatePBDoc("arcaea", userId, chart, log); + expect(pb?.calculatedData.potential).toBeCloseTo(12.19683, 3); + expect(pb).toMatchObject({ composedFrom: [{ name: "Best Score" }, { name: "Best Lamp", scoreID: "bestLamp" }], scoreData: { diff --git a/typescript/server/src/game-implementations/games/arcaea.ts b/typescript/server/src/game-implementations/games/arcaea.ts index 9b28e1688..87f10f82b 100644 --- a/typescript/server/src/game-implementations/games/arcaea.ts +++ b/typescript/server/src/game-implementations/games/arcaea.ts @@ -1,6 +1,6 @@ import { type GameImplementation } from "#game-implementations/types"; import { CreatePBMergeFor } from "#game-implementations/utils/pb-merge"; -import { ProfileAvgBestN } from "#game-implementations/utils/profile-calc"; +import { ProfileSumBestN } from "#game-implementations/utils/profile-calc"; import { SessionAvgBest10For } from "#game-implementations/utils/session-calc"; import { IsNullish } from "#utils/misc"; import { Potential } from "rg-stats"; @@ -24,7 +24,10 @@ export const ARCAEA_IMPL: GameImplementation<"arcaea"> = { grade: GetGrade(ARCAEA_GBOUNDARIES, scoreData.score), }), scoreCalcs: (scoreData, _derivedData, chart) => ({ - potential: chart.levelNum > 0 ? Potential.calculate(scoreData.score, chart.levelNum) : 0, + potential: + chart.levelNum > 0 + ? Potential.calculate(scoreData.score, chart.levelNum, scoreData.lamp) + : 0, }), pbRankingValues: (pb) => ({ ranking: pb.scoreData.score, @@ -38,7 +41,10 @@ export const ARCAEA_IMPL: GameImplementation<"arcaea"> = { naivePotential: SessionAvgBest10For("potential")(arr), }), profileCalcs: async (game, userID) => ({ - naivePotential: await ProfileAvgBestN("potential", 30)(game, userID), + naivePotential: + (((await ProfileSumBestN("potential", 50)(game, userID)) ?? 0) + + ((await ProfileSumBestN("potential", 10)(game, userID)) ?? 0)) / + 60, }), classDerivers: (ratings) => { const potential = ratings.naivePotential; diff --git a/typescript/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/typescript/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index 9aaf046d2..0f9b41416 100644 --- a/typescript/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/typescript/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -15,6 +15,7 @@ import { staticAssertUnreachable, } from "#utils/misc"; import { + FindArcaeaChartOnInGameStrID, FindBMSChartOnHash, FindChartOnInGameIDIfUnique, FindChartOnInGameIDPrimary, @@ -441,6 +442,53 @@ export async function ResolveSongAndChart( return { song, chart }; } + case "arcaeaInGameStrID": { + const identifier = resolver.identifier; + + const config = GetGameConfig("arcaea"); + + if (config.difficulties.type === "DYNAMIC") { + log.error( + { + config, + }, + `Arcaea has 'DYNAMIC' difficulties set. This is completely unexpected.`, + ); + throw new ScoreImportFatalError( + 500, + `Arcaea has 'DYNAMIC' difficulties set. This is completely unexpected.`, + ); + } + + if ( + !config.difficulties.order.includes(resolver.difficulty) && + resolver.difficulty !== "AnyBeyond" + ) { + throw new InvalidScoreFailure( + `Invalid difficulty '${ + resolver.difficulty + }', Expected any of ${config.difficulties.order.join(", ")} or AnyBeyond`, + ); + } + + const diff = resolver.difficulty as "AnyBeyond" | Difficulties["arcaea"]; + + const chart = await FindArcaeaChartOnInGameStrID(identifier, diff); + + if (!chart) { + return null; + } + + const song = await FindSongOnID("arcaea", chart.song.id); + + if (!song) { + log.error(`Song-Chart desync on ${chart.song.id}.`); + throw new InternalFailure(`Failed to get song for a chart that exists.`); + } + + return { song, chart }; + } + case "uscChartHash": { if (gameGroup !== "usc") { throw new InvalidScoreFailure(`uscChartHash matchType can only be used on USC.`); diff --git a/typescript/server/src/utils/queries/charts.ts b/typescript/server/src/utils/queries/charts.ts index e93e0db0c..875d3ce08 100644 --- a/typescript/server/src/utils/queries/charts.ts +++ b/typescript/server/src/utils/queries/charts.ts @@ -563,6 +563,39 @@ export async function FindChartOnInGameStrIDVersion`(chart.data::jsonb->>'inGameStrID') = ${inGameStrID}`) + .where("chart.is_primary", "=", true); + + q = + difficulty === "AnyBeyond" + ? q.where("chart.difficulty", "in", [...ARCAEA_BEYOND_DIFFS]) + : q.where("chart.difficulty", "=", difficulty); + + const row = await q.executeTakeFirst(); + + if (!row) { + return null; + } + + return ToChartDocument(row); +} + /** * Finds an IIDX chart on its 2dxtra hash, which is the sha256 of the .1 buffer. */