diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv3c/converter.ts b/server/src/lib/score-import/import-types/ir/kshook-sv3c/converter.ts new file mode 100644 index 000000000..2d8c18db3 --- /dev/null +++ b/server/src/lib/score-import/import-types/ir/kshook-sv3c/converter.ts @@ -0,0 +1,99 @@ +import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts"; +import { EmptyObject } from "utils/types"; +import { ConverterFunction } from "../../common/types"; +import { KsHookSV3CScore } from "./types"; +import { Lamps } from "tachi-common"; +import { + InternalFailure, + KTDataNotFoundFailure, +} from "lib/score-import/framework/common/converter-failures"; +import { FindSongOnID } from "utils/queries/songs"; +import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils"; +import { DryScore } from "lib/score-import/framework/common/types"; + +export const ConverterIRKShookSV3C: ConverterFunction = async ( + data, + context, + importType, + logger +) => { + const diff = ConvertDifficulty(data.difficulty); + + const chart = await FindSDVXChartOnInGameIDVersion(data.music_id, diff, "konaste"); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${data.music_id} (${diff} for Konaste).`, + importType, + data, + context + ); + } + + const song = await FindSongOnID("sdvx", chart.songID); + + if (!song) { + logger.severe(`Song ${chart.songID} (sdvx) has no parent song?`); + throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`); + } + + const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart); + + const dryScore: DryScore<"sdvx:Single"> = { + game: "sdvx", + service: "kshook SV3C", + comment: null, + importType: "ir/kshook-sv3c", + timeAchieved: Date.now(), + scoreData: { + score: data.score, + percent, + grade, + lamp: ConvertLamp(data.clear), + judgements: { + critical: data.critical, + near: data.near, + miss: data.error, + }, + hitMeta: { + fast: data.early, + slow: data.late, + gauge: data.gauge, + maxCombo: data.max_chain, + }, + }, + scoreMeta: {}, + }; + + return { song, chart, dryScore }; +}; + +function ConvertLamp(clear: KsHookSV3CScore["clear"]): Lamps["sdvx:Single"] { + if (clear === "CLEAR_PLAYED") { + return "FAILED"; + } else if (clear === "CLEAR_EFFECTIVE") { + return "CLEAR"; + } else if (clear === "CLEAR_EXCESSIVE") { + return "EXCESSIVE CLEAR"; + } else if (clear === "CLEAR_ULTIMATE_CHAIN") { + return "ULTIMATE CHAIN"; + } + + return "PERFECT ULTIMATE CHAIN"; +} + +function ConvertDifficulty( + diff: KsHookSV3CScore["difficulty"] +): "NOV" | "ADV" | "EXH" | "ANY_INF" | "MXM" { + if (diff === "DIFFICULTY_NOVICE") { + return "NOV"; + } else if (diff === "DIFFICULTY_ADVANCED") { + return "ADV"; + } else if (diff === "DIFFICULTY_EXHAUST") { + return "EXH"; + } else if (diff === "DIFFICULTY_INFINITE") { + return "ANY_INF"; + } + + return "MXM"; +} diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv3c/parser.ts b/server/src/lib/score-import/import-types/ir/kshook-sv3c/parser.ts new file mode 100644 index 000000000..0de306847 --- /dev/null +++ b/server/src/lib/score-import/import-types/ir/kshook-sv3c/parser.ts @@ -0,0 +1,136 @@ +import { SDVXDans } from "lib/constants/classes"; +import { KtLogger } from "lib/logger/logger"; +import { InvalidScoreFailure } from "lib/score-import/framework/common/converter-failures"; +import p, { PrudenceSchema } from "prudence"; +import { FormatPrError } from "utils/prudence"; +import { EmptyObject } from "utils/types"; +import { ParserFunctionReturns } from "../../common/types"; +import { KsHookSV3CScore } from "./types"; + +const PR_KsHookSV3C: PrudenceSchema = { + appeal_id: p.isInteger, + clear: p.isIn( + "CLEAR_PLAYED", + "CLEAR_EFFECTIVE", + "CLEAR_EXCESSIVE", + "CLEAR_ULTIMATE_CHAIN", + "CLEAR_PERFECT" + ), + difficulty: p.isIn( + "DIFFICULTY_NOVICE", + "DIFFICULTY_ADVANCED", + "DIFFICULTY_EXHAUST", + "DIFFICULTY_INFINITE", + "DIFFICULTY_MAXIMUM" + ), + early: p.isPositiveInteger, + late: p.isPositiveInteger, + gauge: p.isBetween(0, 100), + grade: p.isIn( + "GRADE_D", + "GRADE_C", + "GRADE_B", + "GRADE_A", + "GRADE_A_PLUS", + "GRADE_AA", + "GRADE_AA_PLUS", + "GRADE_AAA", + "GRADE_AAA_PLUS", + "GRADE_S" + ), + max_chain: p.isPositiveInteger, + music_id: p.isPositiveInteger, + + btn_rate: p.isBetween(0, 200), + long_rate: p.isBetween(0, 200), + vol_rate: p.isBetween(0, 200), + + player_name: "string", + rate: p.isIn("RATE_EFFECTIVE", "RATE_PERMISSIVE", "RATE_EXCESSIVE", "RATE_BLASTIVE"), + + skill_frame: p.isIn( + "SKILL_FRAME_NONE", + "SKILL_FRAME_SILVER", + "SKILL_FRAME_GOLD", + "SKILL_FRAME_GOLD_HALO" + ), + skill_level: p.isIn( + "SKILL_LEVEL_NONE", + "SKILL_LEVEL_01", + "SKILL_LEVEL_02", + "SKILL_LEVEL_03", + "SKILL_LEVEL_04", + "SKILL_LEVEL_05", + "SKILL_LEVEL_06", + "SKILL_LEVEL_07", + "SKILL_LEVEL_08", + "SKILL_LEVEL_09", + "SKILL_LEVEL_10", + "SKILL_LEVEL_11", + "SKILL_LEVEL_12" + ), + track_no: p.isPositiveInteger, +}; + +export function ParseKsHookSV3C( + body: Record, + logger: KtLogger +): ParserFunctionReturns { + // Ignore excess keys, as SV3C might add more features in the future. + const err = p(body, PR_KsHookSV3C, undefined, { allowExcessKeys: true }); + + if (err) { + throw new InvalidScoreFailure(FormatPrError(err)); + } + + const score = body as unknown as KsHookSV3CScore; + + return { + game: "sdvx", + iterable: [score], + context: {}, + classHandler: () => { + const skillLevel = ConvertSkillLevel(score.skill_level); + + // If null, the player doesn't have a dan. + if (!skillLevel) { + return; + } + + return { + dan: skillLevel, + }; + }, + }; +} + +function ConvertSkillLevel(skill: KsHookSV3CScore["skill_level"]): SDVXDans | null { + switch (skill) { + case "SKILL_LEVEL_01": + return SDVXDans.DAN_1; + case "SKILL_LEVEL_02": + return SDVXDans.DAN_2; + case "SKILL_LEVEL_03": + return SDVXDans.DAN_3; + case "SKILL_LEVEL_04": + return SDVXDans.DAN_4; + case "SKILL_LEVEL_05": + return SDVXDans.DAN_5; + case "SKILL_LEVEL_06": + return SDVXDans.DAN_6; + case "SKILL_LEVEL_07": + return SDVXDans.DAN_7; + case "SKILL_LEVEL_08": + return SDVXDans.DAN_8; + case "SKILL_LEVEL_09": + return SDVXDans.DAN_9; + case "SKILL_LEVEL_10": + return SDVXDans.DAN_10; + case "SKILL_LEVEL_11": + return SDVXDans.DAN_11; + case "SKILL_LEVEL_12": + return SDVXDans.INF; + default: + return null; + } +} diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv3c/types.ts b/server/src/lib/score-import/import-types/ir/kshook-sv3c/types.ts new file mode 100644 index 000000000..0b7f4e331 --- /dev/null +++ b/server/src/lib/score-import/import-types/ir/kshook-sv3c/types.ts @@ -0,0 +1,58 @@ +import { integer } from "tachi-common"; + +export interface KsHookSV3CScore { + appeal_id: integer; + clear: + | "CLEAR_PLAYED" + | "CLEAR_EFFECTIVE" + | "CLEAR_EXCESSIVE" + | "CLEAR_ULTIMATE_CHAIN" + | "CLEAR_PERFECT"; + difficulty: `DIFFICULTY_${"NOVICE" | "ADVANCED" | "EXHAUST" | "INFINITE" | "MAXIMUM"}`; + early: integer; + gauge: integer; + grade: `GRADE_${ + | "D" + | "C" + | "B" + | "A" + | "A_PLUS" + | "AA" + | "AA_PLUS" + | "AAA" + | "AAA_PLUS" + | "S"}`; + late: integer; + max_chain: integer; + music_id: integer; + + btn_rate: integer; + long_rate: integer; + vol_rate: integer; + + critical: integer; + near: integer; + error: integer; + + player_name: string; + rate: `RATE_${"EFFECTIVE" | "PERMISSIVE" | "EXCESSIVE" | "BLASTIVE"}`; + score: integer; + + skill_frame: `SKILL_FRAME_${"NONE" | "SILVER" | "GOLD" | "GOLD_HALO"}`; + skill_level: `SKILL_LEVEL_${ + | "NONE" + | "01" + | "02" + | "03" + | "04" + | "05" + | "06" + | "07" + | "08" + | "09" + | "10" + | "11" + | "12"}`; + skill_name: integer; + track_no: integer; +}