From 94e59a9cffa448e280cb62560232bdaeaac86473 Mon Sep 17 00:00:00 2001 From: zkldi Date: Thu, 13 May 2021 23:23:52 +0100 Subject: [PATCH] Rough static parser --- .../ir/fervidex-static/converter.ts | 220 ++++++++++++++++++ .../import-types/ir/fervidex-static/parser.ts | 77 ++++++ .../import-types/ir/fervidex-static/types.ts | 13 ++ .../import-types/ir/fervidex/parser.ts | 4 +- 4 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 server/src/score-import/import-types/ir/fervidex-static/converter.ts create mode 100644 server/src/score-import/import-types/ir/fervidex-static/parser.ts create mode 100644 server/src/score-import/import-types/ir/fervidex-static/types.ts diff --git a/server/src/score-import/import-types/ir/fervidex-static/converter.ts b/server/src/score-import/import-types/ir/fervidex-static/converter.ts new file mode 100644 index 000000000..a3dd16b78 --- /dev/null +++ b/server/src/score-import/import-types/ir/fervidex-static/converter.ts @@ -0,0 +1,220 @@ +import { FindSongOnID } from "../../../../common/database-lookup/song"; +import { ConverterFunction, DryScore } from "../../../../types"; +import { + GenericCalculatePercent, + GetGradeFromPercent, +} from "../../../framework/common/score-utils"; +import { + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, +} from "../../../framework/score-importing/converter-failures"; +import { FervidexContext, FervidexScore } from "./types"; +import { Lamps, Grades, Difficulties, Playtypes } from "kamaitachi-common"; +import { FindChartOnInGameIDVersion } from "../../../../common/database-lookup/chart"; + +const LAMP_LOOKUP = { + 0: "NO PLAY", + 1: "FAILED", + 2: "ASSIST CLEAR", + 3: "EASY CLEAR", + 4: "CLEAR", + 5: "HARD CLEAR", + 6: "EX HARD CLEAR", + 7: "FULL COMBO", +}; + +function KtchifyAssist( + assist: FervidexScore["option"]["assist"] +): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["assist"] { + switch (assist) { + case "ASCR_LEGACY": + return "FULL ASSIST"; + case "AUTO_SCRATCH": + return "AUTO SCRATCH"; + case "FULL_ASSIST": + return "FULL ASSIST"; + case "LEGACY_NOTE": + return "LEGACY NOTE"; + case null: + case undefined: + return "NO ASSIST"; + } +} + +function KtchifyGauge( + gauge: FervidexScore["option"]["gauge"] +): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["gauge"] { + switch (gauge) { + case "ASSISTED_EASY": + return "ASSISTED EASY"; + case "EASY": + return "EASY"; + case "EX_HARD": + return "EX-HARD"; + case "HARD": + return "HARD"; + case null: + case undefined: + return "NORMAL"; + } +} + +function KtchifyRange( + gauge: FervidexScore["option"]["range"] +): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["range"] { + switch (gauge) { + case "HIDDEN_PLUS": + return "HIDDEN+"; + case "LIFT": + return "LIFT"; + case "LIFT_SUD_PLUS": + return "LIFT SUD+"; + case "SUDDEN_PLUS": + return "SUDDEN+"; + case "SUD_PLUS_HID_PLUS": + return "SUD+ HID+"; + case null: + case undefined: + return "NONE"; + } +} + +function KtchifyRandom( + gauge: FervidexScore["option"]["style"] +): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["random"] { + switch (gauge) { + case "RANDOM": + return "RANDOM"; + case "S_RANDOM": + return "S-RANDOM"; + case "R_RANDOM": + return "R-RANDOM"; + case "MIRROR": + return "MIRROR"; + case null: + case undefined: + return "NONRAN"; + } +} + +function SplitFervidexChartRef(ferDif: FervidexScore["chart"]) { + let playtype: Playtypes["iidx"]; + if (ferDif.startsWith("sp")) { + playtype = "SP"; + } else { + playtype = "DP"; + } + + let difficulty: Difficulties["iidx:SP" | "iidx:DP"]; + + switch (ferDif[ferDif.length - 1]) { + case "b": + difficulty = "BEGINNER"; + break; + case "n": + difficulty = "NORMAL"; + break; + case "h": + difficulty = "HYPER"; + break; + case "a": + difficulty = "ANOTHER"; + break; + case "l": + difficulty = "LEGGENDARIA"; + break; + default: + throw new InternalFailure(`Invalid fervidex difficulty of ${ferDif}`); + } + + return { playtype, difficulty }; +} + +export const ConverterIRFervidex: ConverterFunction = async ( + data, + context, + importType, + logger +) => { + let { difficulty, playtype } = SplitFervidexChartRef(data.chart); + + let chart = await FindChartOnInGameIDVersion( + "iidx", + data.entry_id, + playtype, + difficulty, + context.version + ); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${data.entry_id} (${playtype} ${difficulty})`, + importType, + data, + context + ); + } + + let song = await FindSongOnID("iidx", chart.songID); + + if (!song) { + logger.severe(`Song ${chart.songID} (iidx) has no parent song?`); + throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`); + } + + let gaugeHistory = data.gauge.map((e) => (e > 200 ? null : e)); + + let gauge = gaugeHistory[gaugeHistory.length - 1]; + + if (gauge && gauge > 100) { + throw new InvalidScoreFailure(`Invalid value of gauge ${gauge}.`); + } + + const percent = GenericCalculatePercent("iidx", data.ex_score); + + if (percent > 100) { + throw new InvalidScoreFailure( + `Invalid score of ${data.ex_score} for chart ${song.title} (${playtype} ${difficulty}). Resulted in percent ${percent}.` + ); + } + + let dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + game: "iidx", + service: "fervidex", + comment: null, + importType: "ir/fervidex", + timeAchieved: Date.now(), + scoreData: { + score: data.ex_score, + percent, + grade: GetGradeFromPercent("iidx", percent) as Grades["iidx:SP" | "iidx:DP"], + lamp: LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], + hitData: { + pgreat: data.pgreat, + great: data.great, + good: data.good, + bad: data.bad, + poor: data.poor, + }, + hitMeta: { + fast: data.fast, + slow: data.slow, + maxCombo: data.max_combo, + gaugeHistory, + gauge, + bp: data.bad + data.poor, + comboBreak: null, + gsm: data["2dx-gsm"], + }, + }, + scoreMeta: { + assist: KtchifyAssist(data.option.assist), + gauge: KtchifyGauge(data.option.gauge), + random: KtchifyRandom(data.option.style), + range: KtchifyRange(data.option.range), + }, + }; + + return { song, chart, dryScore }; +}; diff --git a/server/src/score-import/import-types/ir/fervidex-static/parser.ts b/server/src/score-import/import-types/ir/fervidex-static/parser.ts new file mode 100644 index 000000000..129dfdae2 --- /dev/null +++ b/server/src/score-import/import-types/ir/fervidex-static/parser.ts @@ -0,0 +1,77 @@ +import { KtLogger, ParserFunctionReturnsSync } from "../../../../types"; +import p, { PrudenceSchema } from "prudence"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; +import { FormatPrError, optNull } from "../../../../common/prudence"; +import { ConverterIRFervidex } from "./converter"; +import { FervidexStaticContext, FervidexStaticScore } from "./types"; +import { FerHeaders, ParseSoftwareModel } from "../fervidex/parser"; +import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; + +const PR_FervidexStatic: PrudenceSchema = { + ex_score: p.isPositiveInteger, + miss_count: p.nullable(p.or(p.isPositiveInteger, p.is(-1))), + clear_type: p.isBoundedInteger(0, 7), +}; + +export function ParseFervidexStatic( + body: Record, + headers: FerHeaders, + logger: KtLogger +): ParserFunctionReturnsSync { + let version = ParseSoftwareModel(headers.model); + + let staticScores = body?.scores; + + if (!staticScores || typeof staticScores !== "object") { + throw new ScoreImportFatalError(400, `Invalid body.scores.`); + } + + let scores: FervidexStaticScore[] = []; + + for (const songID in staticScores) { + // @ts-expect-error pls. + let subScores = staticScores[songID]; + + let song_id = AssertStrAsPositiveInt(songID, `Invalid songID ${songID}.`); + + if (!subScores || typeof subScores !== "object") { + throw new ScoreImportFatalError(400, `Invalid score with songID ${songID}.`); + } + + for (const chart in subScores) { + let score = subScores[chart]; + + if (!score || typeof subScores !== "object") { + throw new ScoreImportFatalError( + 400, + `Invalid score with songID ${songID} at chart ${chart}.` + ); + } + + let err = p(score, PR_FervidexStatic); + + if (err) { + throw new ScoreImportFatalError( + 400, + FormatPrError(err, `Invalid Score with songID ${songID} at chart ${chart}`) + ); + } + + scores.push({ + song_id, + chart, + clear_type: score.clear_type, + ex_score: score.ex_score, + miss_count: score.miss_count, + }); + } + } + + // asserted using prudence. + return { + context: { version }, + game: "iidx", + iterable: scores, + ConverterFunction: ConverterIRFervidex, + }; +} diff --git a/server/src/score-import/import-types/ir/fervidex-static/types.ts b/server/src/score-import/import-types/ir/fervidex-static/types.ts new file mode 100644 index 000000000..853f06f10 --- /dev/null +++ b/server/src/score-import/import-types/ir/fervidex-static/types.ts @@ -0,0 +1,13 @@ +import { integer } from "kamaitachi-common"; + +export interface FervidexStaticScore { + clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + ex_score: integer; + miss_count: integer; // bp, not cbrk + chart: string; + song_id: integer; +} + +export interface FervidexStaticContext { + version: string; +} diff --git a/server/src/score-import/import-types/ir/fervidex/parser.ts b/server/src/score-import/import-types/ir/fervidex/parser.ts index 9fad60cf3..34fcd05ca 100644 --- a/server/src/score-import/import-types/ir/fervidex/parser.ts +++ b/server/src/score-import/import-types/ir/fervidex/parser.ts @@ -54,7 +54,7 @@ const PR_Fervidex: PrudenceSchema = { * Converts a string of the form LDJ:X:X:X:2020092900 into a game version. * I don't really understand the software model format, so this is lazy. */ -function ParseSoftwareModel(model: string) { +export function ParseSoftwareModel(model: string) { if (model.startsWith("LDJ")) { // heroic verse if (model.endsWith("2020092900")) { @@ -71,7 +71,7 @@ function ParseSoftwareModel(model: string) { throw new ScoreImportFatalError(400, `Unsupported Software Model ${model}.`); } -interface FerHeaders { +export interface FerHeaders { model: string; }