Baseline untested Fervidex support

This commit is contained in:
zkldi
2021-05-13 22:25:53 +01:00
parent 81e72d1fe4
commit 89eac259a8
4 changed files with 368 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import { Router, RequestHandler } from "express";
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
const router: Router = Router({ mergeParams: true });
const RequireInf2Header: RequestHandler = async (req, res, next) => {
let swModel = req.header("X-Software-Model");
if (!swModel || !swModel.startsWith("P2D:J:B:A")) {
return res.status(400).send();
}
return next();
};
/**
* Submits all of a users data to Kamaitachi. This data is extremely minimal,
* as only a users Lamp and Score are sent. As such, this is not the prefered
* way of syncing scores outside of INF2, where there is no other way to
* retrieve scores.
*
* @name /api/ir/fervidex/profile/submit
*/
router.post("/profile/submit", RequireLoggedIn, RequireInf2Header, async (req, res) => {
throw new Error("Unimplemented.");
});
/**
* Submits a single score to Kamaitachi. In contrast to profile/submit, this
* sends the most data (and most accurate data) of any score hook.
* As such, this is the preferred way of submitting IIDX scores to Kamaitachi.
*
* @name /api/ir/fervidex/score/submit
*/
router.post("/score/submit", RequireLoggedIn, async (req, res) => {
throw new Error("Unimplemented.");
});
/**
* Submits the result of a class to Kamaitachi. This contains the dan played
* and whether it was achieved.
*
* @name /api/ir/fervidex/class/submit
*/
router.post("/class/submit", RequireLoggedIn, async (req, res) => {
throw new Error("Unimplemented.");
});
export default router;
@@ -0,0 +1,211 @@
import { FindSongOnID } from "../../../../common/database-lookup/song";
import { ConverterFunction, DryScore, EmptyObject } from "../../../../types";
import {
GenericCalculatePercent,
GetGradeFromPercent,
} from "../../../framework/common/score-utils";
import {
InternalFailure,
InvalidScoreFailure,
KTDataNotFoundFailure,
} from "../../../framework/score-importing/converter-failures";
import { FervidexScore } from "./types";
import { Lamps, Grades, Difficulties, Playtypes } from "kamaitachi-common";
import { FindChartOnInGameID } 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, EmptyObject> = async (
data,
context,
importType,
logger
) => {
let { difficulty, playtype } = SplitFervidexChartRef(data.chart);
let chart = await FindChartOnInGameID("iidx", data.entry_id, playtype, difficulty);
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,
},
},
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,62 @@
import { EmptyObject, KtLogger, ParserFunctionReturnsSync } from "../../../../types";
import p, { ValidSchemaValue, PrudenceSchema } from "prudence";
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
import { FormatPrError, optNull } from "../../../../common/prudence";
import { FervidexScore } from "./types";
import { ConverterIRFervidex } from "./converter";
const PR_Fervidex: PrudenceSchema = {
chart: p.isIn("spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"),
entry_id: p.isPositiveInteger,
pgreat: p.isPositiveInteger,
great: p.isPositiveInteger,
good: p.isPositiveInteger,
bad: p.isPositiveInteger,
poor: p.isPositiveInteger,
slow: p.isPositiveInteger,
fast: p.isPositiveInteger,
max_combo: p.isPositiveInteger,
ex_score: p.isPositiveInteger,
clear_type: p.isBoundedInteger(0, 7),
gauge: [p.isBoundedInteger(0, 255)],
dead: {
measure: optNull(p.isPositiveInteger),
note: optNull(p.isPositiveInteger),
},
option: {
gauge: optNull(p.isIn("ASSISTED_EASY", "EASY", "HARD", "EX_HARD")),
range: optNull(
p.isIn("SUDDEN_PLUS", "HIDDEN_PLUS", "SUD_PLUS_HID_PLUS", "LIFT", "LIFT_SUD_PLUS")
),
style: optNull(p.isIn("RANDOM", "R_RANDOM", "S_RANDOM", "MIRROR")),
assist: optNull(p.isIn("AUTO_SCRATCH", "LEGACY_NOTE", "ASCR_LEGACY", "FULL_ASSIST")),
},
// we dont use it and we dont care.
pacemaker: p.optional(p.any),
};
export function ParseFervidexSingle(
body: Record<string, unknown>,
logger: KtLogger
): ParserFunctionReturnsSync<FervidexScore, EmptyObject> {
let err = p(body, PR_Fervidex);
if (err) {
throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Fervidex Request?"));
}
// asserted using prudence.
return {
context: {},
game: "iidx",
iterable: ([body] as unknown) as FervidexScore[],
ConverterFunction: ConverterIRFervidex,
};
}
@@ -0,0 +1,46 @@
import { integer } from "kamaitachi-common";
export interface FervidexScore {
chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`;
entry_id: integer;
pgreat: integer;
great: integer;
good: integer;
bad: integer;
poor: integer;
slow: integer;
fast: integer;
max_combo: integer;
ex_score: integer;
clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
gauge: [integer];
dead: {
measure?: integer | null;
note?: integer | null;
};
option: {
gauge?: "ASSISTED_EASY" | "EASY" | "HARD" | "EX_HARD" | null;
range?:
| "SUDDEN_PLUS"
| "HIDDEN_PLUS"
| "SUD_PLUS_HID_PLUS"
| "LIFT"
| "LIFT_SUD_PLUS"
| null;
style?: "RANDOM" | "R_RANDOM" | "S_RANDOM" | "MIRROR";
assist?: "AUTO_SCRATCH" | "LEGACY_NOTE" | "ASCR_LEGACY" | "FULL_ASSIST";
};
pacemaker: {
name?: string | null;
score?: integer | null;
type?: string | null; // too lazy to type this and we dont use it.
};
}