mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-09 07:09:57 +03:00
Baseline untested Fervidex support
This commit is contained in:
@@ -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.
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user