mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-22 15:14:27 +03:00
feat: arcaea 7.0 (#1736)
* fucking lowirommmmmmmmmmmmmmm * I gently open the repo * eagles can fly, people can try, but that's about it * I still hate you * potential merger test * remove raw b50 * touchup * less insane handling of lamps * add sacrosanct
This commit is contained in:
@@ -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';
|
||||
+1192
-2
File diff suppressed because it is too large
Load Diff
@@ -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": "⊥⊬"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<GamesForGroup["arcaea"]>["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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,6 +39,7 @@ const PR_BATCH_MANUAL_SCORE = (game: V3Game): PrudenceSchema => {
|
||||
"uscChartHash",
|
||||
"popnChartHash",
|
||||
"ddrSongHash",
|
||||
"arcaeaInGameStrID",
|
||||
),
|
||||
identifier: "string",
|
||||
comment: optNull(p.isBoundedString(3, 240)),
|
||||
|
||||
@@ -21,6 +21,7 @@ type MatchTypesNoDifficulty =
|
||||
|
||||
// These MatchTypes need `difficulty` set in the batch manual.
|
||||
type MatchTypesWithDifficulty =
|
||||
| "arcaeaInGameStrID"
|
||||
| "ddrSongHash"
|
||||
| "inGameID"
|
||||
| "inGameStrID"
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type LocalizedSearchTerms = Record<string, Array<string>>;
|
||||
|
||||
interface SonglistChart {
|
||||
ratingClass: integer;
|
||||
ratingClassAlias?: integer;
|
||||
title_localized?: LocalizedText;
|
||||
artist?: string;
|
||||
audioOverride?: boolean;
|
||||
@@ -93,7 +94,7 @@ class MultiMapUniqueValues<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -29,6 +29,7 @@ const inverseConvertDifficulty = (input: string) => {
|
||||
case "Future":
|
||||
return 2;
|
||||
case "Beyond":
|
||||
case "Inscribed":
|
||||
return 3;
|
||||
case "Eternal":
|
||||
return 4;
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -563,6 +563,39 @@ export async function FindChartOnInGameStrIDVersion<TGame extends V3Game = V3Gam
|
||||
return ToChartDocument(row);
|
||||
}
|
||||
|
||||
const ARCAEA_BEYOND_DIFFS = ["Beyond", "Inscribed"] as const;
|
||||
|
||||
/**
|
||||
* Find an Arcaea Chart on its in game string ID. This exists to handle
|
||||
* oddities with Arcaea difficulties - If "AnyBeyond" is sent, it actually
|
||||
* refers to any of Beyond or Inscribed. This is because some services treat
|
||||
* all of those as the same difficulty, but we do not.
|
||||
*/
|
||||
export async function FindArcaeaChartOnInGameStrID(
|
||||
inGameStrID: string,
|
||||
difficulty: "AnyBeyond" | Difficulties[GamesForGroup["arcaea"]],
|
||||
) {
|
||||
let q = DB.selectFrom("chart")
|
||||
.innerJoin("song", "song.id", "chart.song_id")
|
||||
.select(SELECT_CHART)
|
||||
.where("chart.game", "=", "arcaea")
|
||||
.where(sql<boolean>`(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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user