Rough static parser

This commit is contained in:
zkldi
2021-05-13 23:23:52 +01:00
parent 408d7c3621
commit 94e59a9cff
4 changed files with 312 additions and 2 deletions
@@ -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<FervidexScore, FervidexContext> = 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 };
};
@@ -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<string, unknown>,
headers: FerHeaders,
logger: KtLogger
): ParserFunctionReturnsSync<FervidexStaticScore, FervidexStaticContext> {
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,
};
}
@@ -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;
}
@@ -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;
}