mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 10:28:04 +03:00
Merge pull request #190 from zkldi/feature/config-refactor
Config Refactor
This commit is contained in:
@@ -65,7 +65,6 @@
|
||||
"default-param-last": "error",
|
||||
"no-new-func": "error",
|
||||
"arrow-body-style": "error",
|
||||
// "arrow-parens": ["error", "as-needed"] prettier hates this
|
||||
"no-useless-constructor": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"dot-notation": "error",
|
||||
|
||||
Generated
+9
-231
File diff suppressed because it is too large
Load Diff
@@ -12,133 +12,133 @@ const logger = CreateLogCtx(__filename);
|
||||
// HERE. WE. GO!
|
||||
|
||||
function ConditionalAssign(base: any, baseProp: string, other: any, otherProp: string) {
|
||||
if (Object.prototype.hasOwnProperty.call(other, otherProp)) {
|
||||
base[baseProp] = other[otherProp];
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(other, otherProp)) {
|
||||
base[baseProp] = other[otherProp];
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFn(c: any): ScoreDocument | null {
|
||||
if (!supportedGames.includes(c.game)) {
|
||||
logger.warn(`Ignored game ${c.game}`);
|
||||
return null;
|
||||
}
|
||||
if (!supportedGames.includes(c.game)) {
|
||||
logger.warn(`Ignored game ${c.game}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (["popn", "jubeat", "gitadora", "usc"].includes(c.game)) {
|
||||
return null;
|
||||
}
|
||||
if (["popn", "jubeat", "gitadora", "usc"].includes(c.game)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (validPlaytypes[c.game].includes(c.playtype)) {
|
||||
logger.warn(`Ignored game pt ${c.game}, ${c.playtype}`);
|
||||
}
|
||||
if (validPlaytypes[c.game].includes(c.playtype)) {
|
||||
logger.warn(`Ignored game pt ${c.game}, ${c.playtype}`);
|
||||
}
|
||||
|
||||
const base: Omit<ScoreDocument, "scoreID"> = {
|
||||
userID: c.userID,
|
||||
songID: c.songID,
|
||||
playtype: c.scoreData.playtype,
|
||||
chartID: c.chartID,
|
||||
game: c.game,
|
||||
timeAdded: c.timeAdded,
|
||||
timeAchieved: Number.isNaN(c.timeAchieved) ? null : c.timeAchieved,
|
||||
comment: c.comment ?? null,
|
||||
highlight: c.highlight ?? false,
|
||||
service: c.service,
|
||||
importType: null,
|
||||
calculatedData: c.calculatedData,
|
||||
scoreData: {
|
||||
esd: c.scoreData.esd ?? null,
|
||||
grade: c.scoreData.grade,
|
||||
gradeIndex: grades[c.game].indexOf(c.scoreData.grade),
|
||||
lamp: c.scoreData.lamp,
|
||||
lampIndex: lamps[c.game].indexOf(c.scoreData.lamp),
|
||||
percent: c.scoreData.percent,
|
||||
score: c.scoreData.score,
|
||||
hitData: c.scoreData.hitData,
|
||||
hitMeta: c.scoreData.hitMeta,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
const base: Omit<ScoreDocument, "scoreID"> = {
|
||||
userID: c.userID,
|
||||
songID: c.songID,
|
||||
playtype: c.scoreData.playtype,
|
||||
chartID: c.chartID,
|
||||
game: c.game,
|
||||
timeAdded: c.timeAdded,
|
||||
timeAchieved: Number.isNaN(c.timeAchieved) ? null : c.timeAchieved,
|
||||
comment: c.comment ?? null,
|
||||
highlight: c.highlight ?? false,
|
||||
service: c.service,
|
||||
importType: null,
|
||||
calculatedData: c.calculatedData,
|
||||
scoreData: {
|
||||
esd: c.scoreData.esd ?? null,
|
||||
grade: c.scoreData.grade,
|
||||
gradeIndex: grades[c.game].indexOf(c.scoreData.grade),
|
||||
lamp: c.scoreData.lamp,
|
||||
lampIndex: lamps[c.game].indexOf(c.scoreData.lamp),
|
||||
percent: c.scoreData.percent,
|
||||
score: c.scoreData.score,
|
||||
judgements: c.scoreData.judgements,
|
||||
hitMeta: c.scoreData.hitMeta,
|
||||
},
|
||||
scoreMeta: {},
|
||||
};
|
||||
|
||||
if (c.scoreMeta) {
|
||||
if (base.game === "iidx") {
|
||||
ConditionalAssign(base.scoreMeta, "random", c.scoreMeta, "optionsRandom");
|
||||
ConditionalAssign(base.scoreMeta, "gauge", c.scoreMeta, "optionsGauge");
|
||||
ConditionalAssign(base.scoreMeta, "assist", c.scoreMeta, "optionsAssist");
|
||||
ConditionalAssign(base.scoreMeta, "range", c.scoreMeta, "optionsRange");
|
||||
ConditionalAssign(base.scoreMeta, "pacemaker", c.scoreMeta, "pacemaker");
|
||||
ConditionalAssign(base.scoreMeta, "pacemakerName", c.scoreMeta, "pacemakerName");
|
||||
ConditionalAssign(base.scoreMeta, "pacemakerTarget", c.scoreMeta, "pacemakerTarget");
|
||||
if (c.scoreMeta) {
|
||||
if (base.game === "iidx") {
|
||||
ConditionalAssign(base.scoreMeta, "random", c.scoreMeta, "optionsRandom");
|
||||
ConditionalAssign(base.scoreMeta, "gauge", c.scoreMeta, "optionsGauge");
|
||||
ConditionalAssign(base.scoreMeta, "assist", c.scoreMeta, "optionsAssist");
|
||||
ConditionalAssign(base.scoreMeta, "range", c.scoreMeta, "optionsRange");
|
||||
ConditionalAssign(base.scoreMeta, "pacemaker", c.scoreMeta, "pacemaker");
|
||||
ConditionalAssign(base.scoreMeta, "pacemakerName", c.scoreMeta, "pacemakerName");
|
||||
ConditionalAssign(base.scoreMeta, "pacemakerTarget", c.scoreMeta, "pacemakerTarget");
|
||||
|
||||
if (base.playtype === "DP" && !Array.isArray(base.scoreMeta.random)) {
|
||||
base.scoreMeta.random = null;
|
||||
}
|
||||
if (base.playtype === "DP" && !Array.isArray(base.scoreMeta.random)) {
|
||||
base.scoreMeta.random = null;
|
||||
}
|
||||
|
||||
// @ts-expect-error asdf
|
||||
if (base.scoreMeta.range === "") {
|
||||
// @ts-expect-error asdf
|
||||
base.scoreMeta.range = "NONE";
|
||||
}
|
||||
} else if (base.game === "bms") {
|
||||
ConditionalAssign(base.scoreMeta, "random", c.scoreMeta, "optionsRandom");
|
||||
ConditionalAssign(base.scoreMeta, "inputDevice", c.scoreData.hitMeta, "inputDevice");
|
||||
}
|
||||
}
|
||||
// @ts-expect-error asdf
|
||||
if (base.scoreMeta.range === "") {
|
||||
// @ts-expect-error asdf
|
||||
base.scoreMeta.range = "NONE";
|
||||
}
|
||||
} else if (base.game === "bms") {
|
||||
ConditionalAssign(base.scoreMeta, "random", c.scoreMeta, "optionsRandom");
|
||||
ConditionalAssign(base.scoreMeta, "inputDevice", c.scoreData.hitMeta, "inputDevice");
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error shut
|
||||
if (base.scoreData.hitMeta.bp === -1 || Number.isNaN(base.scoreData.hitMeta.bp)) {
|
||||
// @ts-expect-error shut
|
||||
base.scoreData.hitMeta.bp = null;
|
||||
}
|
||||
// @ts-expect-error shut
|
||||
if (base.scoreData.hitMeta.bp === -1 || Number.isNaN(base.scoreData.hitMeta.bp)) {
|
||||
// @ts-expect-error shut
|
||||
base.scoreData.hitMeta.bp = null;
|
||||
}
|
||||
|
||||
if (base.game === "iidx") {
|
||||
// @ts-expect-error nope
|
||||
if (base.scoreData.hitMeta.gauge > 200) {
|
||||
// @ts-expect-error nope
|
||||
base.scoreData.hitMeta.gauge = null;
|
||||
}
|
||||
if (base.game === "iidx") {
|
||||
// @ts-expect-error nope
|
||||
if (base.scoreData.hitMeta.gauge > 200) {
|
||||
// @ts-expect-error nope
|
||||
base.scoreData.hitMeta.gauge = null;
|
||||
}
|
||||
|
||||
// @ts-expect-error nope
|
||||
if (base.scoreData.hitMeta.gaugeHistory) {
|
||||
// @ts-expect-error nope
|
||||
base.scoreData.hitMeta.gaugeHistory = base.scoreData.hitMeta.gaugeHistory.map((e) =>
|
||||
e > 200 ? null : e
|
||||
);
|
||||
}
|
||||
} else if (base.game === "bms") {
|
||||
// @ts-expect-error shut
|
||||
if (base.scoreData.hitMeta.gauge === -1) {
|
||||
// @ts-expect-error shut
|
||||
base.scoreData.hitMeta.gauge = null;
|
||||
}
|
||||
// @ts-expect-error nope
|
||||
if (base.scoreData.hitMeta.gaugeHistory) {
|
||||
// @ts-expect-error nope
|
||||
base.scoreData.hitMeta.gaugeHistory = base.scoreData.hitMeta.gaugeHistory.map((e) =>
|
||||
e > 200 ? null : e
|
||||
);
|
||||
}
|
||||
} else if (base.game === "bms") {
|
||||
// @ts-expect-error shut
|
||||
if (base.scoreData.hitMeta.gauge === -1) {
|
||||
// @ts-expect-error shut
|
||||
base.scoreData.hitMeta.gauge = null;
|
||||
}
|
||||
|
||||
// @ts-expect-error shut
|
||||
delete base.scoreData.hitMeta.inputDevice;
|
||||
} else if (base.game === "sdvx" || base.game === "usc") {
|
||||
ConditionalAssign(base.scoreData.hitData, "miss", base.scoreData.hitData, "error");
|
||||
// @ts-expect-error shut
|
||||
delete base.scoreData.hitMeta.inputDevice;
|
||||
} else if (base.game === "sdvx" || base.game === "usc") {
|
||||
ConditionalAssign(base.scoreData.judgements, "miss", base.scoreData.judgements, "error");
|
||||
|
||||
//@ts-expect-error yea
|
||||
delete base.scoreData.hitData.error;
|
||||
} else if (base.game === "chunithm") {
|
||||
if (base.scoreData.percent === 101) {
|
||||
base.scoreData.grade === "SSS";
|
||||
}
|
||||
}
|
||||
//@ts-expect-error yea
|
||||
delete base.scoreData.judgements.error;
|
||||
} else if (base.game === "chunithm") {
|
||||
if (base.scoreData.percent === 101) {
|
||||
base.scoreData.grade === "SSS";
|
||||
}
|
||||
}
|
||||
|
||||
if (base.service === "mm") {
|
||||
base.service = "maimagic";
|
||||
}
|
||||
if (base.service === "mm") {
|
||||
base.service = "maimagic";
|
||||
}
|
||||
|
||||
const scoreID = CreateScoreID(base.userID, base, base.chartID);
|
||||
const scoreID = CreateScoreID(base.userID, base, base.chartID);
|
||||
|
||||
const score: ScoreDocument = {
|
||||
...base,
|
||||
scoreID,
|
||||
};
|
||||
const score: ScoreDocument = {
|
||||
...base,
|
||||
scoreID,
|
||||
};
|
||||
|
||||
return score;
|
||||
return score;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await MigrateRecords(db.scores, "scores", ConvertFn);
|
||||
await MigrateRecords(db.scores, "scores", ConvertFn);
|
||||
|
||||
process.exit(0);
|
||||
process.exit(0);
|
||||
})();
|
||||
|
||||
+142
-142
@@ -16,180 +16,180 @@ const BASE_DIR = path.join(__dirname, "./validate-database-errs");
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
async function ValidateStaticSchemas(): Promise<void> {
|
||||
for (const x in STATIC_SCHEMAS) {
|
||||
const c = x as keyof typeof STATIC_SCHEMAS;
|
||||
for (const x in STATIC_SCHEMAS) {
|
||||
const c = x as keyof typeof STATIC_SCHEMAS;
|
||||
|
||||
const schema = STATIC_SCHEMAS[c] as PrudenceSchema;
|
||||
const schema = STATIC_SCHEMAS[c] as PrudenceSchema;
|
||||
|
||||
logger.info(`=== Validating Collection ${c}... ===`);
|
||||
logger.info(`=== Validating Collection ${c}... ===`);
|
||||
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
|
||||
const start = process.hrtime.bigint();
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
// @ts-expect-error shut UP
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db[c].find({}, { projection: { _id: 0 } }).each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
// @ts-expect-error shut UP
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db[c].find({}, { projection: { _id: 0 } }).each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(res);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(res);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(path.join(BASE_DIR, `${c}.json`), JSON.stringify(fails));
|
||||
}
|
||||
}
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(path.join(BASE_DIR, `${c}.json`), JSON.stringify(fails));
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Done!");
|
||||
process.exit(0);
|
||||
logger.info("Done!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function ValidateScores(): Promise<void> {
|
||||
const globalStart = process.hrtime.bigint();
|
||||
let globalTotal = 0;
|
||||
let globalSuccess = 0;
|
||||
let globalFail = 0;
|
||||
for (const game of CONF_INFO.SUPPORTED_GAMES) {
|
||||
for (const playtype of validPlaytypes[game]) {
|
||||
// @ts-expect-error shut up
|
||||
const schema = PRUDENCE_SCORE_SCHEMAS[game][playtype];
|
||||
const globalStart = process.hrtime.bigint();
|
||||
let globalTotal = 0;
|
||||
let globalSuccess = 0;
|
||||
let globalFail = 0;
|
||||
for (const game of CONF_INFO.supportedGames) {
|
||||
for (const playtype of validPlaytypes[game]) {
|
||||
// @ts-expect-error shut up
|
||||
const schema = PRUDENCE_SCORE_SCHEMAS[game][playtype];
|
||||
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
|
||||
logger.info(`=== Validating Collection scores (${game} ${playtype}) ... ===`);
|
||||
const start = process.hrtime.bigint();
|
||||
logger.info(`=== Validating Collection scores (${game} ${playtype}) ... ===`);
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.scores
|
||||
.find({ playtype, game }, { projection: { _id: 0 } })
|
||||
// @ts-expect-error shut UP
|
||||
.each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db.scores
|
||||
.find({ playtype, game }, { projection: { _id: 0 } })
|
||||
// @ts-expect-error shut UP
|
||||
.each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(
|
||||
`[${game} ${playtype}] [${res.keychain}] ${res.message}: ${res.userVal}`
|
||||
);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(
|
||||
`[${game} ${playtype}] [${res.keychain}] ${res.message}: ${res.userVal}`
|
||||
);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `scores-${game}-${playtype}.json`),
|
||||
JSON.stringify(fails)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `scores-${game}-${playtype}.log`),
|
||||
`Validated ${total} objects. Took ${
|
||||
Number(end - start) / 1e9
|
||||
} seconds.\nSuccess: ${successCount} (${((successCount * 100) / total).toFixed(
|
||||
2
|
||||
)}%)\nFail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`
|
||||
);
|
||||
}
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `scores-${game}-${playtype}.json`),
|
||||
JSON.stringify(fails)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `scores-${game}-${playtype}.log`),
|
||||
`Validated ${total} objects. Took ${
|
||||
Number(end - start) / 1e9
|
||||
} seconds.\nSuccess: ${successCount} (${((successCount * 100) / total).toFixed(
|
||||
2
|
||||
)}%)\nFail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`
|
||||
);
|
||||
}
|
||||
|
||||
globalTotal += total;
|
||||
globalFail += fails.length;
|
||||
globalSuccess += successCount;
|
||||
}
|
||||
}
|
||||
globalTotal += total;
|
||||
globalFail += fails.length;
|
||||
globalSuccess += successCount;
|
||||
}
|
||||
}
|
||||
|
||||
const globalEnd = process.hrtime.bigint();
|
||||
logger.info(
|
||||
`Validated ${globalTotal} objects. Took ${Number(globalEnd - globalStart) / 1e9} seconds.`
|
||||
);
|
||||
logger.info(`Success: ${globalSuccess} (${((globalSuccess * 100) / globalTotal).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${globalFail} (${((globalFail * 100) / globalTotal).toFixed(2)}%)`);
|
||||
const globalEnd = process.hrtime.bigint();
|
||||
logger.info(
|
||||
`Validated ${globalTotal} objects. Took ${Number(globalEnd - globalStart) / 1e9} seconds.`
|
||||
);
|
||||
logger.info(`Success: ${globalSuccess} (${((globalSuccess * 100) / globalTotal).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${globalFail} (${((globalFail * 100) / globalTotal).toFixed(2)}%)`);
|
||||
|
||||
// process.exit(1);
|
||||
// process.exit(1);
|
||||
}
|
||||
|
||||
async function ValidateChartsOrSongs(type: "songs" | "charts"): Promise<void> {
|
||||
const globalStart = process.hrtime.bigint();
|
||||
let globalTotal = 0;
|
||||
let globalSuccess = 0;
|
||||
let globalFail = 0;
|
||||
for (const game of CONF_INFO.SUPPORTED_GAMES) {
|
||||
const schema = STATIC_SCHEMAS[type]![game];
|
||||
const globalStart = process.hrtime.bigint();
|
||||
let globalTotal = 0;
|
||||
let globalSuccess = 0;
|
||||
let globalFail = 0;
|
||||
for (const game of CONF_INFO.supportedGames) {
|
||||
const schema = STATIC_SCHEMAS[type]![game];
|
||||
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
let successCount = 0;
|
||||
let total = 0;
|
||||
const fails: { err: PrudenceError; doc: unknown }[] = [];
|
||||
|
||||
logger.info(`=== Validating Collection ${type} (${game}) ... ===`);
|
||||
const start = process.hrtime.bigint();
|
||||
logger.info(`=== Validating Collection ${type} (${game}) ... ===`);
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db[type][game]
|
||||
// @ts-expect-error whatever bro
|
||||
.find({}, { projection: { _id: 0 } })
|
||||
.each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await db[type][game]
|
||||
// @ts-expect-error whatever bro
|
||||
.find({}, { projection: { _id: 0 } })
|
||||
.each((c: unknown) => {
|
||||
total++;
|
||||
const res = Prudence(c, schema);
|
||||
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(`[${game}] [${res.keychain}] ${res.message}: ${res.userVal}`);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
if (res === null) {
|
||||
successCount++;
|
||||
} else {
|
||||
logger.error(`[${game}] [${res.keychain}] ${res.message}: ${res.userVal}`);
|
||||
fails.push({ err: res, doc: c });
|
||||
}
|
||||
});
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
const end = process.hrtime.bigint();
|
||||
logger.info(`Validated ${total} objects. Took ${Number(end - start) / 1e9} seconds.`);
|
||||
logger.info(`Success: ${successCount} (${((successCount * 100) / total).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`);
|
||||
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(path.join(BASE_DIR, `${type}-${game}.json`), JSON.stringify(fails));
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `${type}-${game}.log`),
|
||||
`Validated ${total} objects. Took ${
|
||||
Number(end - start) / 1e9
|
||||
} seconds.\nSuccess: ${successCount} (${((successCount * 100) / total).toFixed(
|
||||
2
|
||||
)}%)\nFail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`
|
||||
);
|
||||
}
|
||||
if (fails.length !== 0) {
|
||||
logger.severe(`Invalid documents found! Please resolve them.`);
|
||||
fs.writeFileSync(path.join(BASE_DIR, `${type}-${game}.json`), JSON.stringify(fails));
|
||||
fs.writeFileSync(
|
||||
path.join(BASE_DIR, `${type}-${game}.log`),
|
||||
`Validated ${total} objects. Took ${
|
||||
Number(end - start) / 1e9
|
||||
} seconds.\nSuccess: ${successCount} (${((successCount * 100) / total).toFixed(
|
||||
2
|
||||
)}%)\nFail: ${fails.length} (${((fails.length * 100) / total).toFixed(2)}%)`
|
||||
);
|
||||
}
|
||||
|
||||
globalTotal += total;
|
||||
globalFail += fails.length;
|
||||
globalSuccess += successCount;
|
||||
}
|
||||
globalTotal += total;
|
||||
globalFail += fails.length;
|
||||
globalSuccess += successCount;
|
||||
}
|
||||
|
||||
const globalEnd = process.hrtime.bigint();
|
||||
logger.info(
|
||||
`Validated ${globalTotal} objects. Took ${Number(globalEnd - globalStart) / 1e9} seconds.`
|
||||
);
|
||||
logger.info(`Success: ${globalSuccess} (${((globalSuccess * 100) / globalTotal).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${globalFail} (${((globalFail * 100) / globalTotal).toFixed(2)}%)`);
|
||||
const globalEnd = process.hrtime.bigint();
|
||||
logger.info(
|
||||
`Validated ${globalTotal} objects. Took ${Number(globalEnd - globalStart) / 1e9} seconds.`
|
||||
);
|
||||
logger.info(`Success: ${globalSuccess} (${((globalSuccess * 100) / globalTotal).toFixed(2)}%)`);
|
||||
logger.info(`Fail: ${globalFail} (${((globalFail * 100) / globalTotal).toFixed(2)}%)`);
|
||||
}
|
||||
|
||||
// ValidateStaticSchemas();
|
||||
|
||||
Vendored
+1
-1
@@ -75,7 +75,7 @@ const staticIndexes: Partial<Record<ValidDatabases, Index[]>> = {
|
||||
|
||||
const indexes: Partial<Record<ValidDatabases, Index[]>> = staticIndexes;
|
||||
|
||||
for (const game of CONF_INFO.SUPPORTED_GAMES) {
|
||||
for (const game of CONF_INFO.supportedGames) {
|
||||
if (indexes[`charts-${game}` as ValidDatabases]) {
|
||||
indexes[`charts-${game}` as ValidDatabases]!.push(
|
||||
index({ chartID: 1 }, UNIQUE),
|
||||
|
||||
Vendored
+54
-48
@@ -1,13 +1,5 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import { Game, Playtypes } from "tachi-common";
|
||||
import {
|
||||
grades,
|
||||
importTypes,
|
||||
lamps,
|
||||
validHitData,
|
||||
validDifficulties,
|
||||
validPlaytypes,
|
||||
} from "tachi-common/js/config";
|
||||
import { Game, Playtypes, GetGameConfig, GetGamePTConfig } from "tachi-common";
|
||||
import p, { PrudenceSchema, ValidSchemaValue } from "prudence";
|
||||
import { CONF_INFO } from "../../lib/setup/config";
|
||||
|
||||
@@ -75,7 +67,7 @@ export const PR_SCORE_GENERIC = {
|
||||
grade: "string",
|
||||
lampIndex: p.isPositiveInteger,
|
||||
gradeIndex: p.isPositiveInteger,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
@@ -100,7 +92,7 @@ export const PR_SCORE_GENERIC = {
|
||||
comment: p.nullable(p.isBoundedString(1, 240)),
|
||||
timeAdded: p.isPositive,
|
||||
scoreID: "string", // temp
|
||||
importType: p.nullable(p.isIn(importTypes)),
|
||||
importType: p.nullable(p.isIn(CONF_INFO.supportedImportTypes)),
|
||||
};
|
||||
|
||||
type ScoreSchemas = {
|
||||
@@ -115,28 +107,35 @@ const nullableAndOptional = (fn: ValidSchemaValue) => p.optional(p.nullable(fn))
|
||||
|
||||
const nullableAndOptionalPosInt = nullableAndOptional(p.isPositiveInteger);
|
||||
|
||||
const CreateGameScoreData = (game: Game, hitMetaMerge: PrudenceSchema) => ({
|
||||
lamp: p.isIn(lamps[game]),
|
||||
lampIndex: p.and(
|
||||
p.isPositiveInteger,
|
||||
(self, parent) => lamps[game][self as number] === parent.lamp
|
||||
),
|
||||
grade: p.isIn(grades[game]),
|
||||
gradeIndex: p.and(
|
||||
p.isPositiveInteger,
|
||||
(self, parent) => grades[game][self as number] === parent.grade
|
||||
),
|
||||
hitData: Object.fromEntries(validHitData[game].map((e) => [e, optionalPositiveInt])),
|
||||
hitMeta: deepmerge(
|
||||
{
|
||||
fast: nullableAndOptionalPosInt,
|
||||
slow: nullableAndOptionalPosInt,
|
||||
maxCombo: nullableAndOptionalPosInt,
|
||||
},
|
||||
hitMetaMerge
|
||||
),
|
||||
esd: p.nullable(p.isPositive),
|
||||
});
|
||||
const CreateGameScoreData = (
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
hitMetaMerge: PrudenceSchema
|
||||
) => {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
return {
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
lampIndex: p.and(
|
||||
p.isPositiveInteger,
|
||||
(self, parent) => gptConfig.lamps[self as number] === parent.lamp
|
||||
),
|
||||
grade: p.isIn(gptConfig.grades),
|
||||
gradeIndex: p.and(
|
||||
p.isPositiveInteger,
|
||||
(self, parent) => gptConfig.grades[self as number] === parent.grade
|
||||
),
|
||||
judgements: Object.fromEntries(gptConfig.judgements.map((e) => [e, optionalPositiveInt])),
|
||||
hitMeta: deepmerge(
|
||||
{
|
||||
fast: nullableAndOptionalPosInt,
|
||||
slow: nullableAndOptionalPosInt,
|
||||
maxCombo: nullableAndOptionalPosInt,
|
||||
},
|
||||
hitMetaMerge
|
||||
),
|
||||
esd: p.nullable(p.isPositive),
|
||||
};
|
||||
};
|
||||
|
||||
const PR_SCORE_IIDX_SP: PrudenceSchema = CreatePRScore(
|
||||
"iidx",
|
||||
@@ -202,7 +201,7 @@ function CreatePRScore<G extends Game>(
|
||||
return deepmerge(PR_SCORE_GENERIC, {
|
||||
game: p.equalTo(game),
|
||||
playtype: p.equalTo(playtype),
|
||||
scoreData: CreateGameScoreData(game, mergeHitMeta),
|
||||
scoreData: CreateGameScoreData(game, playtype, mergeHitMeta),
|
||||
scoreMeta: mergeScoreMeta,
|
||||
calculatedData: {
|
||||
gameSpecific: mergeGameSpecific,
|
||||
@@ -300,9 +299,9 @@ export const PRUDENCE_SCORE_SCHEMAS: ScoreSchemas = {
|
||||
Gita: CreatePRScore("gitadora", "Gita"),
|
||||
Dora: CreatePRScore("gitadora", "Dora"),
|
||||
},
|
||||
jubeat: {
|
||||
Single: CreatePRScore("jubeat", "Single"),
|
||||
},
|
||||
// jubeat: {
|
||||
// Single: CreatePRScore("jubeat", "Single"),
|
||||
// },
|
||||
maimai: {
|
||||
Single: deepmerge(CreatePRScore("maimai", "Single"), {
|
||||
scoreData: { percent: p.isBetween(0, 150) },
|
||||
@@ -311,9 +310,9 @@ export const PRUDENCE_SCORE_SCHEMAS: ScoreSchemas = {
|
||||
museca: {
|
||||
Single: CreatePRScore("museca", "Single"),
|
||||
},
|
||||
popn: {
|
||||
"9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
// popn: {
|
||||
// "9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
// },
|
||||
usc: {
|
||||
Single: CreatePRScore("usc", "Single", { gauge: p.optional(p.isBetween(0, 100)) }),
|
||||
},
|
||||
@@ -330,9 +329,16 @@ const PRUDENCE_CHART_BASE: PrudenceSchema = {
|
||||
};
|
||||
|
||||
function CreatePrChart(game: Game, flags: string[], data: PrudenceSchema) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
const validDifficulties = [];
|
||||
for (const playtype of gameConfig.validPlaytypes) {
|
||||
validDifficulties.push(...GetGamePTConfig(game, playtype).difficulties);
|
||||
}
|
||||
|
||||
return deepmerge(PRUDENCE_CHART_BASE, {
|
||||
difficulty: p.isIn(validDifficulties[game]),
|
||||
playtype: p.isIn(validPlaytypes[game]),
|
||||
difficulty: p.isIn(validDifficulties),
|
||||
playtype: p.isIn(gameConfig.validPlaytypes),
|
||||
flags: Object.fromEntries(flags.map((e) => [e, "boolean"])),
|
||||
data,
|
||||
});
|
||||
@@ -355,13 +361,13 @@ export const PRUDENCE_CHART_SCHEMAS: Record<Game, PrudenceSchema> = {
|
||||
gitadora: CreatePrChart("gitadora", ["IN BASE GAME", "OMNIMIX", "HOT N-1", "HOT"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
jubeat: CreatePrChart("jubeat", ["IN BASE GAME"], {}),
|
||||
// jubeat: CreatePrChart("jubeat", ["IN BASE GAME"], {}),
|
||||
maimai: CreatePrChart("maimai", ["IN BASE GAME"], {
|
||||
maxPercent: p.gte(100),
|
||||
inGameID: "string",
|
||||
}),
|
||||
museca: CreatePrChart("museca", ["IN BASE GAME", "OMNIMIX"], {}),
|
||||
popn: CreatePrChart("popn", ["IN BASE GAME", "OMNIMIX"], {}),
|
||||
// popn: CreatePrChart("popn", ["IN BASE GAME", "OMNIMIX"], {}),
|
||||
sdvx: CreatePrChart("sdvx", ["IN BASE GAME", "OMNIMIX", "N-1"], {
|
||||
inGameID: p.isPositiveInteger,
|
||||
}),
|
||||
@@ -393,10 +399,10 @@ export const PRUDENCE_SONG_SCHEMAS: Record<Game, PrudenceSchema> = {
|
||||
chunithm: PRUDENCE_SONG_WITH_GENRE,
|
||||
ddr: CreatePrSong({}),
|
||||
gitadora: CreatePrSong({}),
|
||||
jubeat: CreatePrSong({}),
|
||||
// jubeat: CreatePrSong({}),
|
||||
maimai: CreatePrSong({ titleJP: "string", artistJP: "string", genre: "string" }),
|
||||
museca: CreatePrSong({ titleJP: "string", artistJP: "string" }),
|
||||
popn: PRUDENCE_SONG_WITH_GENRE,
|
||||
// popn: PRUDENCE_SONG_WITH_GENRE,
|
||||
sdvx: CreatePrSong({ uscEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
usc: CreatePrSong({ sdvxEquiv: p.nullable(p.isPositiveInteger) }),
|
||||
};
|
||||
@@ -408,8 +414,8 @@ const PRUDENCE_FOLDER_CHART_LOOKUP: PrudenceSchema = {
|
||||
|
||||
const PRUDENCE_FOLDER: PrudenceSchema = {
|
||||
title: "string",
|
||||
game: p.isIn(CONF_INFO.SUPPORTED_GAMES),
|
||||
playtype: (self, parent) => p.isIn(validPlaytypes[parent.game as Game])(self),
|
||||
game: p.isIn(CONF_INFO.supportedGames),
|
||||
playtype: (self, parent) => p.isIn(GetGameConfig(parent.game as Game).validPlaytypes)(self),
|
||||
folderID: "string",
|
||||
table: "string",
|
||||
tableIndex: "number",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../test-utils/test-data";
|
||||
import { CreateFolderChartLookup } from "../../utils/folder";
|
||||
import { CloseAllConnections } from "../../test-utils/close-connections";
|
||||
import { Random20Hex } from "../../utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -208,17 +209,21 @@ t.test("#EvaluateGoalForUser", (t) => {
|
||||
delete Testing511SPA._id;
|
||||
|
||||
await db.charts.iidx.insert([
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 123,
|
||||
level: "9",
|
||||
chartID: "not_sp10",
|
||||
data: {
|
||||
arcChartID: Random20Hex(),
|
||||
},
|
||||
}),
|
||||
// @ts-expect-error ???
|
||||
deepmerge(Testing511SPA, {
|
||||
songID: 124,
|
||||
level: "10",
|
||||
chartID: "other_sp10",
|
||||
data: {
|
||||
arcChartID: Random20Hex(),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -298,10 +303,10 @@ t.test("#EvaluateGoalForUser", (t) => {
|
||||
});
|
||||
|
||||
t.test("#HumaniseGoalProgress", (t) => {
|
||||
t.equal(HumaniseGoalProgress("iidx", "scoreData.gradeIndex", 4, null), "B");
|
||||
t.equal(HumaniseGoalProgress("iidx", "scoreData.lampIndex", 4, null), "CLEAR");
|
||||
t.equal(HumaniseGoalProgress("iidx", "scoreData.percent", 90.1142, null), "90.11%");
|
||||
t.equal(HumaniseGoalProgress("iidx", "scoreData.score", 2240, null), "2240");
|
||||
t.equal(HumaniseGoalProgress("iidx", "SP", "scoreData.gradeIndex", 4, null), "B");
|
||||
t.equal(HumaniseGoalProgress("iidx", "SP", "scoreData.lampIndex", 4, null), "CLEAR");
|
||||
t.equal(HumaniseGoalProgress("iidx", "SP", "scoreData.percent", 90.1142, null), "90.11%");
|
||||
t.equal(HumaniseGoalProgress("iidx", "SP", "scoreData.score", 2240, null), "2240");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { GoalDocument, integer, PBScoreDocument, Game } from "tachi-common";
|
||||
import { grades, lamps } from "tachi-common/js/config";
|
||||
import {
|
||||
GoalDocument,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
Playtypes,
|
||||
} from "tachi-common";
|
||||
import db from "../../external/mongo/db";
|
||||
import { KtLogger } from "../logger/logger";
|
||||
import { GetFolderChartIDs } from "../../utils/folder";
|
||||
@@ -69,6 +75,7 @@ export async function EvaluateGoalForUser(
|
||||
|
||||
const outOfHuman = HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value,
|
||||
null
|
||||
@@ -82,6 +89,7 @@ export async function EvaluateGoalForUser(
|
||||
outOfHuman,
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
goal.criteria.key,
|
||||
res.scoreData[scoreDataKey],
|
||||
res
|
||||
@@ -120,6 +128,7 @@ export async function EvaluateGoalForUser(
|
||||
progress: nextBestScore.scoreData[scoreDataKey],
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
goal.criteria.key,
|
||||
nextBestScore.scoreData[scoreDataKey],
|
||||
nextBestScore
|
||||
@@ -192,20 +201,22 @@ type IIDXOrBMSPB = PBScoreDocument<"iidx:SP" | "iidx:DP" | "bms:7K" | "bms:14K">
|
||||
// @todo, #100 improve this (add things like BP for iidx, maybe, percents for scores?)
|
||||
export function HumaniseGoalProgress(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
key: GoalKeys,
|
||||
value: number,
|
||||
userPB: PBScoreDocument | null
|
||||
): string {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
switch (key) {
|
||||
case "scoreData.gradeIndex":
|
||||
return grades[game][value];
|
||||
return gptConfig.grades[value];
|
||||
case "scoreData.lampIndex":
|
||||
if (userPB && (game === "iidx" || game === "bms")) {
|
||||
return `${lamps[game][value]} (BP: ${
|
||||
return `${gptConfig.lamps[value]} (BP: ${
|
||||
(userPB as IIDXOrBMSPB).scoreData.hitMeta.bp ?? "N/A"
|
||||
})`;
|
||||
}
|
||||
return lamps[game][value];
|
||||
return gptConfig.lamps[value];
|
||||
case "scoreData.percent":
|
||||
return `${value.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
|
||||
@@ -347,43 +347,43 @@ t.test("#CalculateDataForGamePT", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("jubeat:Single", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"jubeat",
|
||||
"Single",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
// t.test("jubeat:Single", async (t) => {
|
||||
// const res = await CalculateDataForGamePT(
|
||||
// "jubeat",
|
||||
// "Single",
|
||||
// Testing511SPA,
|
||||
// TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
// null,
|
||||
// defaultTierlistID,
|
||||
// logger
|
||||
// );
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
jubility: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for jubeat:Single"
|
||||
);
|
||||
// prAssert(
|
||||
// res,
|
||||
// {
|
||||
// jubility: "?number",
|
||||
// },
|
||||
// "Response should contain nulled keys for jubeat:Single"
|
||||
// );
|
||||
|
||||
t.end();
|
||||
});
|
||||
// t.end();
|
||||
// });
|
||||
|
||||
t.test("popn:9B", async (t) => {
|
||||
const res = await CalculateDataForGamePT(
|
||||
"popn",
|
||||
"9B",
|
||||
Testing511SPA,
|
||||
TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
null,
|
||||
defaultTierlistID,
|
||||
logger
|
||||
);
|
||||
// t.test("popn:9B", async (t) => {
|
||||
// const res = await CalculateDataForGamePT(
|
||||
// "popn",
|
||||
// "9B",
|
||||
// Testing511SPA,
|
||||
// TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing
|
||||
// null,
|
||||
// defaultTierlistID,
|
||||
// logger
|
||||
// );
|
||||
|
||||
prAssert(res, {}, "Response should contain nulled keys for popn:9B");
|
||||
// prAssert(res, {}, "Response should contain nulled keys for popn:9B");
|
||||
|
||||
t.end();
|
||||
});
|
||||
// t.end();
|
||||
// });
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -69,9 +69,9 @@ const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
sdvx: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
// popn: {
|
||||
// "9B": () => ({}),
|
||||
// },
|
||||
museca: {
|
||||
Single: CalculateDataMuseca,
|
||||
},
|
||||
@@ -93,9 +93,9 @@ const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
SP: CalculateDataDDR,
|
||||
DP: CalculateDataDDR,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateDataJubeat,
|
||||
},
|
||||
// jubeat: {
|
||||
// Single: CalculateDataJubeat,
|
||||
// },
|
||||
usc: {
|
||||
Single: CalculateDataSDVXorUSC,
|
||||
},
|
||||
@@ -310,13 +310,13 @@ async function CalculateDataBMS7K(
|
||||
};
|
||||
}
|
||||
|
||||
async function CalculateDataJubeat(
|
||||
dryScore: DryScore,
|
||||
chart: AnyChartDocument,
|
||||
defaultTierlistID: string | undefined,
|
||||
logger: KtLogger
|
||||
): Promise<CalculatedData<"jubeat:Single">> {
|
||||
return {
|
||||
jubility: 0, // @todo #163 Jubeat Jubility
|
||||
};
|
||||
}
|
||||
// async function CalculateDataJubeat(
|
||||
// dryScore: DryScore,
|
||||
// chart: AnyChartDocument,
|
||||
// defaultTierlistID: string | undefined,
|
||||
// logger: KtLogger
|
||||
// ): Promise<CalculatedData<"jubeat:Single">> {
|
||||
// return {
|
||||
// jubility: 0, // @todo #163 Jubeat Jubility
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { AnyChartDocument, ESDCore, integer, Grades, Lamps, Game, Playtypes } from "tachi-common";
|
||||
import {
|
||||
AnyChartDocument,
|
||||
ESDCore,
|
||||
integer,
|
||||
Grades,
|
||||
Lamps,
|
||||
Game,
|
||||
Playtypes,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
} from "tachi-common";
|
||||
import db from "../../../../external/mongo/db";
|
||||
import { GetAllTierlistDataOfType, GetOneTierlistData } from "../../../../utils/tierlist";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import { DryScore } from "../common/types";
|
||||
import { lamps, clearLamp, ratingParameters } from "tachi-common/js/config";
|
||||
|
||||
/**
|
||||
* Calculates the in-game CHUNITHM rating for a score.
|
||||
@@ -305,9 +314,37 @@ function RatingCalcV0Fail(percentDiv100: number, levelNum: number, parameters: R
|
||||
);
|
||||
}
|
||||
|
||||
const ratingParameters = {
|
||||
iidx: {
|
||||
failHarshnessMultiplier: 0.3,
|
||||
pivotPercent: 0.7777, // Grade: AA
|
||||
clearExpMultiplier: 1,
|
||||
},
|
||||
bms: {
|
||||
failHarshnessMultiplier: 0.5,
|
||||
pivotPercent: 0.7777, // Grade: AA
|
||||
clearExpMultiplier: 0.75,
|
||||
},
|
||||
museca: {
|
||||
failHarshnessMultiplier: 1,
|
||||
pivotPercent: 0.8, // grade: not fail
|
||||
clearExpMultiplier: 1, // no real reason
|
||||
},
|
||||
maimai: {
|
||||
failHarshnessMultiplier: 1,
|
||||
pivotPercent: 0.8,
|
||||
clearExpMultiplier: 1,
|
||||
},
|
||||
ddr: {
|
||||
failHarshnessMultiplier: 0.9,
|
||||
pivotPercent: 0.9,
|
||||
clearExpMultiplier: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export async function CalculateKTRating(
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
game: "iidx" | "bms" | "ddr" | "maimai" | "museca",
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument,
|
||||
logger: KtLogger,
|
||||
@@ -316,7 +353,7 @@ export async function CalculateKTRating(
|
||||
// If this game doesn't have a specific rating function declared, fall back to the default "generic" rating function.
|
||||
// This is just a function that is guaranteed to work for all input - and therefore not result in lower-skilled users
|
||||
// always seeing 0.
|
||||
const parameters = ratingParameters[dryScore.game];
|
||||
const parameters = ratingParameters[game];
|
||||
|
||||
let levelNum = chart.levelNum;
|
||||
|
||||
@@ -346,21 +383,25 @@ export async function CalculateKTLampRating(
|
||||
) {
|
||||
// if no tierlist data
|
||||
if (!defaultTierlistID) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
return LampRatingNoTierlistInfo(dryScore, game, playtype, chart);
|
||||
}
|
||||
|
||||
const lampTierlistInfo = await GetAllTierlistDataOfType(game, chart, "lamp", defaultTierlistID);
|
||||
|
||||
// if no tierlist info
|
||||
if (lampTierlistInfo.length === 0) {
|
||||
return LampRatingNoTierlistInfo(dryScore, game, chart);
|
||||
return LampRatingNoTierlistInfo(dryScore, game, playtype, chart);
|
||||
}
|
||||
|
||||
const userLampIndex = lamps[game].indexOf(dryScore.scoreData.lamp);
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const lamps = gptConfig.lamps;
|
||||
|
||||
const userLampIndex = lamps.indexOf(dryScore.scoreData.lamp);
|
||||
|
||||
// if this score is a clear, the lowest we should go is the levelNum of the chart.
|
||||
// Else, the lowest we can go is 0.
|
||||
let lampRating = userLampIndex >= lamps[game].indexOf(clearLamp[game]) ? chart.levelNum : 0;
|
||||
let lampRating = userLampIndex >= lamps.indexOf(gptConfig.clearLamp) ? chart.levelNum : 0;
|
||||
|
||||
// why is this like this and not a lookup table?
|
||||
// Some charts have higher values for *lower* lamps, such as
|
||||
@@ -371,7 +412,7 @@ export async function CalculateKTLampRating(
|
||||
// this tierlistData has higher value than the current lampRating
|
||||
tierlistData.data.value > lampRating &&
|
||||
// and your clear is better than the lamp its for
|
||||
lamps[game].indexOf(tierlistData.key!) <= userLampIndex
|
||||
lamps.indexOf(tierlistData.key as Lamps[IDStrings]) <= userLampIndex
|
||||
) {
|
||||
lampRating = tierlistData.data.value;
|
||||
}
|
||||
@@ -380,11 +421,20 @@ export async function CalculateKTLampRating(
|
||||
return lampRating;
|
||||
}
|
||||
|
||||
function LampRatingNoTierlistInfo(dryScore: DryScore, game: Game, chart: AnyChartDocument) {
|
||||
const CLEAR_LAMP_INDEX = lamps[game].indexOf(clearLamp[game]);
|
||||
function LampRatingNoTierlistInfo(
|
||||
dryScore: DryScore,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
chart: AnyChartDocument
|
||||
) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const lamps = gptConfig.lamps;
|
||||
|
||||
const CLEAR_LAMP_INDEX = lamps.indexOf(gptConfig.clearLamp);
|
||||
|
||||
// if this is a clear
|
||||
if (lamps[game].indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) {
|
||||
if (lamps.indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) {
|
||||
// return this chart's numeric level as the lamp rating
|
||||
return chart.levelNum;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
config,
|
||||
Game,
|
||||
Grades,
|
||||
IDStrings,
|
||||
@@ -8,8 +7,8 @@ import {
|
||||
GameToIDStrings,
|
||||
ESDCore,
|
||||
Playtypes,
|
||||
GetGamePTConfig,
|
||||
} from "tachi-common";
|
||||
import { judgementWindows, gamePercentMax } from "tachi-common/js/config";
|
||||
import CreateLogCtx from "../../../logger/logger";
|
||||
import { InternalFailure, InvalidScoreFailure } from "./converter-failures";
|
||||
|
||||
@@ -20,13 +19,14 @@ const logger = CreateLogCtx(__filename);
|
||||
*/
|
||||
export function GetGradeFromPercent<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
percent: number
|
||||
): Grades[I] {
|
||||
// @todo #102 update config to use game->pt
|
||||
const boundaries = config.gradeBoundaries[game];
|
||||
const grades = config.grades[game];
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const boundaries = gptConfig.gradeBoundaries;
|
||||
const grades = gptConfig.grades;
|
||||
|
||||
// eslint doesn't like backwards for loops (hey, this for loop is backwards!)
|
||||
// (hey, this for loop is backwards!)
|
||||
for (let i = boundaries.length; i >= 0; i--) {
|
||||
if (percent >= boundaries[i]) {
|
||||
return grades[i] as Grades[I];
|
||||
@@ -49,14 +49,13 @@ export function GenericCalculatePercent(
|
||||
switch (game) {
|
||||
case "ddr":
|
||||
case "museca":
|
||||
case "jubeat":
|
||||
case "chunithm":
|
||||
return (score / 1_000_000) * 100;
|
||||
case "sdvx":
|
||||
case "usc":
|
||||
return (score / 10_000_000) * 100;
|
||||
case "popn":
|
||||
return (score / 100_000) * 100;
|
||||
// case "popn":
|
||||
// return (score / 100_000) * 100;
|
||||
case "gitadora":
|
||||
case "maimai":
|
||||
return score;
|
||||
@@ -88,7 +87,12 @@ export function GenericCalculatePercent(
|
||||
*
|
||||
* This exists to support maimai, as it has a dynamic "max percent".
|
||||
*/
|
||||
export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocument) {
|
||||
export function ValidatePercent(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
percent: number,
|
||||
chart: AnyChartDocument
|
||||
) {
|
||||
// i love needing a helper function for *ONE* game.
|
||||
if (game === "maimai") {
|
||||
const mmChart = chart as ChartDocument<"maimai:Single">;
|
||||
@@ -99,9 +103,11 @@ export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocu
|
||||
}
|
||||
}
|
||||
|
||||
if (percent > gamePercentMax[game]) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (percent > gptConfig.percentMax) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid percent of ${percent} - expected a value less than ${gamePercentMax[game]}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).`
|
||||
`Invalid percent of ${percent} - expected a value less than ${gptConfig.percentMax}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -118,9 +124,9 @@ export function GenericGetGradeAndPercent<G extends Game>(
|
||||
) {
|
||||
const percent = GenericCalculatePercent(game, score, chart);
|
||||
|
||||
ValidatePercent(game, percent, chart);
|
||||
ValidatePercent(game, chart.playtype, percent, chart);
|
||||
|
||||
const grade = GetGradeFromPercent(game, percent) as Grades[GameToIDStrings[G]];
|
||||
const grade = GetGradeFromPercent(game, chart.playtype, percent) as Grades[GameToIDStrings[G]];
|
||||
|
||||
return { percent, grade };
|
||||
}
|
||||
@@ -134,20 +140,13 @@ export function CalculateESDForGame(
|
||||
playtype: Playtypes[Game],
|
||||
percent: number
|
||||
): number | null {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(judgementWindows, game) &&
|
||||
// @ts-expect-error i know better
|
||||
Object.prototype.hasOwnProperty.call(judgementWindows[game], playtype)
|
||||
) {
|
||||
if (percent > 0.1) {
|
||||
// @ts-expect-error i know better
|
||||
return ESDCore.CalculateESD(judgementWindows[game][playtype], percent);
|
||||
} else {
|
||||
return 200;
|
||||
}
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (!gptConfig.supportsESD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return ESDCore.CalculateESD(gptConfig.judgementWindows, percent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { InvalidScoreFailure } from "./converter-failures";
|
||||
import { Difficulties, Game, IDStrings, Playtypes } from "tachi-common";
|
||||
import { validDifficulties } from "tachi-common/js/config";
|
||||
|
||||
import { Difficulties, Game, IDStrings, Playtypes, GetGamePTConfig } from "tachi-common";
|
||||
export function AssertStrAsDifficulty(
|
||||
strVal: string,
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
): Difficulties[IDStrings] {
|
||||
if (!validDifficulties[game].includes(strVal)) {
|
||||
const validDifficulties = GetGamePTConfig(game, playtype).difficulties;
|
||||
|
||||
if (!validDifficulties.includes(strVal as Difficulties[IDStrings])) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties[
|
||||
game
|
||||
].join(", ")}`
|
||||
`Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,15 @@ const batchManualScore: BatchManualScore = {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songTitle",
|
||||
identifier: "5.1.1.",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
};
|
||||
|
||||
const batchManualContext: BatchManualContext = { game: "iidx", service: "foo", version: "27" };
|
||||
const batchManualContext: BatchManualContext = {
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
service: "foo",
|
||||
version: "27",
|
||||
};
|
||||
|
||||
t.test("#OrphanScore", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -141,7 +145,7 @@ t.test("#ReprocessOrphan", (t) => {
|
||||
score: 500,
|
||||
grade: "E",
|
||||
percent: 31.806615776081426,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
highlight: false,
|
||||
@@ -228,6 +232,7 @@ t.test("#ReprocessOrphan", (t) => {
|
||||
deepmerge(orphanDoc, {
|
||||
context: {
|
||||
game: "bms",
|
||||
playtype: "7K",
|
||||
},
|
||||
data: {
|
||||
matchType: "bmsChartHash",
|
||||
|
||||
@@ -5,14 +5,15 @@ import { GetKTDataJSON, Testing511SPA, TestingIIDXSPScore } from "../../../../te
|
||||
import { CreatePBDoc } from "./create-pb-doc";
|
||||
import deepmerge from "deepmerge";
|
||||
import CreateLogCtx, { KtLogger } from "../../../logger/logger";
|
||||
|
||||
import { lamps } from "tachi-common/js/config";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { CloseAllConnections } from "../../../../test-utils/close-connections";
|
||||
|
||||
let IIDXScore = TestingIIDXSPScore;
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const lamps = GetGamePTConfig("iidx", "SP").lamps;
|
||||
|
||||
t.test("#CreatePBDoc", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => {
|
||||
@@ -43,9 +44,9 @@ t.test("#CreatePBDoc", (t) => {
|
||||
esd: IIDXScore.scoreData.esd,
|
||||
grade: IIDXScore.scoreData.grade,
|
||||
gradeIndex: IIDXScore.scoreData.gradeIndex,
|
||||
hitData: IIDXScore.scoreData.hitData,
|
||||
judgements: IIDXScore.scoreData.judgements,
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
lampIndex: lamps.indexOf("FULL COMBO"),
|
||||
hitMeta: { bp: 1 },
|
||||
},
|
||||
calculatedData: {
|
||||
@@ -63,7 +64,7 @@ t.test("#CreatePBDoc", (t) => {
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
lampIndex: lamps.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
@@ -78,7 +79,7 @@ t.test("#CreatePBDoc", (t) => {
|
||||
deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "CLEAR",
|
||||
lampIndex: lamps.iidx.indexOf("CLEAR"),
|
||||
lampIndex: lamps.indexOf("CLEAR"),
|
||||
score: 1,
|
||||
percent: 1,
|
||||
hitMeta: {
|
||||
@@ -122,7 +123,7 @@ t.test("#CreatePBDoc", (t) => {
|
||||
const d = deepmerge(IIDXScore, {
|
||||
scoreData: {
|
||||
lamp: "FULL COMBO",
|
||||
lampIndex: lamps.iidx.indexOf("FULL COMBO"),
|
||||
lampIndex: lamps.indexOf("FULL COMBO"),
|
||||
score: 0,
|
||||
percent: 0,
|
||||
hitMeta: {
|
||||
|
||||
@@ -141,7 +141,7 @@ async function MergeScoreLampIntoPB(
|
||||
gradeIndex: scorePB.scoreData.gradeIndex,
|
||||
lamp: lampPB.scoreData.lamp,
|
||||
lampIndex: lampPB.scoreData.lampIndex,
|
||||
hitData: scorePB.scoreData.hitData,
|
||||
judgements: scorePB.scoreData.judgements,
|
||||
hitMeta: scorePB.scoreData.hitMeta, // this will probably be overrode by game-specific fns
|
||||
},
|
||||
calculatedData: scorePB.calculatedData,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AnyChartDocument, config, integer, ScoreDocument, AnySongDocument } from "tachi-common";
|
||||
import {
|
||||
AnyChartDocument,
|
||||
integer,
|
||||
ScoreDocument,
|
||||
AnySongDocument,
|
||||
GetGamePTConfig,
|
||||
} from "tachi-common";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import { CreateCalculatedData } from "../calculated-data/calculated-data";
|
||||
import { CalculateESDForGame } from "../common/score-utils";
|
||||
@@ -27,12 +33,13 @@ export async function HydrateScore(
|
||||
|
||||
const { scoreData: dryScoreData, ...rest } = dryScore;
|
||||
|
||||
const gptConfig = GetGamePTConfig(dryScore.game, chart.playtype);
|
||||
|
||||
// Fill out the rest of the fields we want for scoreData
|
||||
const scoreData = Object.assign(
|
||||
{
|
||||
// @todo #103 lamps may need to be separate upon game:playtype someday. Maybe. We need to check this out
|
||||
lampIndex: config.lamps[dryScore.game].indexOf(dryScore.scoreData.lamp as string),
|
||||
gradeIndex: config.grades[dryScore.game].indexOf(dryScore.scoreData.grade as string),
|
||||
lampIndex: gptConfig.lamps.indexOf(dryScore.scoreData.lamp),
|
||||
gradeIndex: gptConfig.grades.indexOf(dryScore.scoreData.grade),
|
||||
esd,
|
||||
},
|
||||
dryScoreData
|
||||
|
||||
@@ -64,9 +64,9 @@ const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
};
|
||||
},
|
||||
},
|
||||
popn: {
|
||||
"9B": () => ({}),
|
||||
},
|
||||
// popn: {
|
||||
// "9B": () => ({}),
|
||||
// },
|
||||
museca: {
|
||||
Single: (scd) => ({
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
@@ -108,11 +108,11 @@ const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
ktRating: AvgBest10Map(scd, "ktRating"),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: (scd) => ({
|
||||
jubility: AvgBest10Map(scd, "jubility"),
|
||||
}),
|
||||
},
|
||||
// jubeat: {
|
||||
// Single: (scd) => ({
|
||||
// jubility: AvgBest10Map(scd, "jubility"),
|
||||
// }),
|
||||
// },
|
||||
usc: {
|
||||
Single: (scd) => {
|
||||
const VF6 = AvgBest10Map(scd, "VF6");
|
||||
|
||||
@@ -94,13 +94,13 @@ export function GitadoraSkillToColour(sk: number) {
|
||||
return GitadoraColours.WHITE;
|
||||
}
|
||||
|
||||
export function CalculateJubeatColour(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
ratings: Record<string, number>
|
||||
): GameClasses<"jubeat:Single"> {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
// export function CalculateJubeatColour(
|
||||
// game: Game,
|
||||
// playtype: Playtypes[Game],
|
||||
// userID: integer,
|
||||
// ratings: Record<string, number>
|
||||
// ): GameClasses<"jubeat:Single"> {
|
||||
// throw new Error("Not implemented.");
|
||||
// }
|
||||
|
||||
// function JubilityToColour(jb: number) {}
|
||||
|
||||
@@ -2,11 +2,7 @@ import { Game, Playtypes, integer, UserGameStats, ClassDelta, IDStrings } from "
|
||||
import { GameClasses } from "tachi-common/js/game-classes";
|
||||
import deepmerge from "deepmerge";
|
||||
import { KtLogger } from "../../../logger/logger";
|
||||
import {
|
||||
CalculateGitadoraColour,
|
||||
CalculateJubeatColour,
|
||||
CalculateSDVXClass,
|
||||
} from "./builtin-class-handlers";
|
||||
import { CalculateGitadoraColour, CalculateSDVXClass } from "./builtin-class-handlers";
|
||||
import { ReturnClassIfGreater } from "../../../../utils/class";
|
||||
import { RedisPub } from "../../../../external/redis/redis-IPC";
|
||||
import { ClassHandler, ScoreClasses } from "./types";
|
||||
@@ -37,12 +33,12 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
|
||||
Gita: CalculateGitadoraColour,
|
||||
Dora: CalculateGitadoraColour,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateJubeatColour,
|
||||
},
|
||||
// jubeat: {
|
||||
// Single: CalculateJubeatColour,
|
||||
// },
|
||||
maimai: null,
|
||||
museca: null,
|
||||
popn: null,
|
||||
// popn: null,
|
||||
sdvx: {
|
||||
Single: CalculateSDVXClass,
|
||||
},
|
||||
|
||||
@@ -9,14 +9,6 @@ const logger = CreateLogCtx(__filename);
|
||||
t.test("#CalculateRatings", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should work for games with no custom ratings", async (t) => {
|
||||
const res = await CalculateRatings("popn", "9B", 1, logger);
|
||||
|
||||
t.strictSame(res, {}, "Should return an empty object.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return BPI for IIDX", async (t) => {
|
||||
const res = await CalculateRatings("iidx", "SP", 1, logger);
|
||||
|
||||
|
||||
@@ -117,9 +117,9 @@ const RatingFunctions: RatingFunctions = {
|
||||
naiveRating: await LazyMeanN("rating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
popn: {
|
||||
"9B": async () => ({}),
|
||||
},
|
||||
// popn: {
|
||||
// "9B": async () => ({}),
|
||||
// },
|
||||
museca: {
|
||||
Single: async (g, p, u) => ({
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
@@ -130,11 +130,11 @@ const RatingFunctions: RatingFunctions = {
|
||||
ktRating: await LazyMeanN("ktRating", 20)(g, p, u),
|
||||
}),
|
||||
},
|
||||
jubeat: {
|
||||
Single: async (g, p, u, l) => ({
|
||||
jubility: await CalculateJubility(g, p, u, l),
|
||||
}),
|
||||
},
|
||||
// jubeat: {
|
||||
// Single: async (g, p, u, l) => ({
|
||||
// jubility: await CalculateJubility(g, p, u, l),
|
||||
// }),
|
||||
// },
|
||||
};
|
||||
|
||||
export function CalculateRatings(
|
||||
@@ -188,43 +188,43 @@ async function CalculateGitadoraSkill(
|
||||
return skill;
|
||||
}
|
||||
|
||||
async function CalculateJubility(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): Promise<number> {
|
||||
const hotCharts = await db.charts.jubeat.find(
|
||||
{ "flags.HOT N-1": true },
|
||||
{ projection: { chartID: 1 } }
|
||||
);
|
||||
// async function CalculateJubility(
|
||||
// game: Game,
|
||||
// playtype: Playtypes[Game],
|
||||
// userID: integer,
|
||||
// logger: KtLogger
|
||||
// ): Promise<number> {
|
||||
// const hotCharts = await db.charts.jubeat.find(
|
||||
// { "flags.HOT N-1": true },
|
||||
// { projection: { chartID: 1 } }
|
||||
// );
|
||||
|
||||
const hotChartIDs = hotCharts.map((e) => e.chartID);
|
||||
// const hotChartIDs = hotCharts.map((e) => e.chartID);
|
||||
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
db["personal-bests"].find(
|
||||
{ userID, chartID: { $in: hotChartIDs } },
|
||||
{
|
||||
sort: { "calculatedData.jubility": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.jubility": 1 },
|
||||
}
|
||||
),
|
||||
// @inefficient
|
||||
// see gitadoraskillcalc
|
||||
db["personal-bests"].find(
|
||||
{ userID, chartID: { $nin: hotChartIDs } },
|
||||
{
|
||||
sort: { "calculatedData.jubility": -1 },
|
||||
limit: 25,
|
||||
projection: { "calculatedData.jubility": 1 },
|
||||
}
|
||||
),
|
||||
]);
|
||||
// const [bestHotScores, bestScores] = await Promise.all([
|
||||
// db["personal-bests"].find(
|
||||
// { userID, chartID: { $in: hotChartIDs } },
|
||||
// {
|
||||
// sort: { "calculatedData.jubility": -1 },
|
||||
// limit: 25,
|
||||
// projection: { "calculatedData.jubility": 1 },
|
||||
// }
|
||||
// ),
|
||||
// // @inefficient
|
||||
// // see gitadoraskillcalc
|
||||
// db["personal-bests"].find(
|
||||
// { userID, chartID: { $nin: hotChartIDs } },
|
||||
// {
|
||||
// sort: { "calculatedData.jubility": -1 },
|
||||
// limit: 25,
|
||||
// projection: { "calculatedData.jubility": 1 },
|
||||
// }
|
||||
// ),
|
||||
// ]);
|
||||
|
||||
let skill = 0;
|
||||
skill += bestHotScores.reduce((a, r) => a + r.calculatedData.jubility!, 0);
|
||||
skill += bestScores.reduce((a, r) => a + r.calculatedData.jubility!, 0);
|
||||
// let skill = 0;
|
||||
// skill += bestHotScores.reduce((a, r) => a + r.calculatedData.jubility!, 0);
|
||||
// skill += bestScores.reduce((a, r) => a + r.calculatedData.jubility!, 0);
|
||||
|
||||
return skill;
|
||||
}
|
||||
// return skill;
|
||||
// }
|
||||
|
||||
@@ -32,7 +32,7 @@ t.test("#ConvertAPIArcDDR", (t) => {
|
||||
grade: "AAA",
|
||||
percent: 99.976,
|
||||
score: 807,
|
||||
hitData: {
|
||||
judgements: {
|
||||
marvelous: 236,
|
||||
perfect: 24,
|
||||
great: 0,
|
||||
|
||||
@@ -90,7 +90,7 @@ export const ConvertAPIArcDDR: ConverterFunction<unknown, EmptyObject> = async (
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {
|
||||
judgements: {
|
||||
marvelous: score.judgments.marvelous,
|
||||
perfect: score.judgments.perfect,
|
||||
great: score.judgments.great,
|
||||
|
||||
@@ -29,7 +29,7 @@ t.test("#ConvertAPIArcIIDX", (t) => {
|
||||
scoreData: {
|
||||
grade: "A",
|
||||
score: 1200,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: 43,
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@ export const ConvertAPIArcIIDX: ConverterFunction<unknown, EmptyObject> = async
|
||||
grade,
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: score.miss_count,
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ t.test("#ConvertAPIArcSDVX", (t) => {
|
||||
grade: "AA",
|
||||
percent: 93.125,
|
||||
score: 9312500,
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: 946,
|
||||
near: 45,
|
||||
miss: 49,
|
||||
|
||||
@@ -74,7 +74,7 @@ export const ConvertAPIArcSDVX: ConverterFunction<unknown, EmptyObject> = async
|
||||
grade,
|
||||
percent,
|
||||
score: score.score,
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: score.critical,
|
||||
near: score.near,
|
||||
miss: score.error,
|
||||
|
||||
@@ -35,7 +35,7 @@ t.test("#ConvertAPIKaiIIDX", (t) => {
|
||||
// percent: 99.87277353689568 floating point,
|
||||
score: 1570,
|
||||
lamp: "HARD CLEAR",
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
fast: null,
|
||||
slow: null,
|
||||
|
||||
@@ -108,7 +108,7 @@ export const ConvertAPIKaiIIDX: ConverterFunction<unknown, KaiContext> = async (
|
||||
percent,
|
||||
score: score.ex_score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
fast: score.fast_count,
|
||||
slow: score.slow_count,
|
||||
|
||||
@@ -35,7 +35,7 @@ t.test("#ConvertAPIKaiSDVX", (t) => {
|
||||
// percent: 93.10699, floating point
|
||||
score: 9310699,
|
||||
lamp: "EXCESSIVE CLEAR",
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
fast: 70,
|
||||
slow: 42,
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ConvertAPIKaiSDVX: ConverterFunction<unknown, KaiContext> = async (
|
||||
percent,
|
||||
score: score.score,
|
||||
lamp,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
fast: score.early,
|
||||
slow: score.late,
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function* TraverseKaiAPI(
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": `${CONF_INFO.NAME}/${VERSION_STR}`,
|
||||
"User-Agent": `${CONF_INFO.name}/${VERSION_STR}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const baseBatchManualScore = {
|
||||
|
||||
const context = {
|
||||
game: "iidx" as const,
|
||||
playtype: "SP" as const,
|
||||
service: "foo",
|
||||
version: null,
|
||||
};
|
||||
@@ -105,7 +106,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => {
|
||||
const gazerSong = GetKTDataJSON("./tachi/bms-gazer-song.json");
|
||||
const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json");
|
||||
|
||||
const bmsContext: any = deepmerge(context, { game: "bms" });
|
||||
const bmsContext: any = deepmerge(context, { game: "bms", playtype: "7K" });
|
||||
|
||||
const resMD5 = await ResolveMatchTypeToKTData(
|
||||
deepmerge(baseBatchManualScore, {
|
||||
@@ -244,7 +245,7 @@ t.test("#ResolveChartFromSong", (t) => {
|
||||
const res = await ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
baseBatchManualScore, // has playtype + diff
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", playtype: "SP", version: null },
|
||||
importType
|
||||
);
|
||||
|
||||
@@ -253,30 +254,13 @@ t.test("#ResolveChartFromSong", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw an error if no playtype is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
deepmerge(baseBatchManualScore, { playtype: null }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but was necessary for this lookup.`
|
||||
) as any
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw an error if no difficulty is provided.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ResolveChartFromSong(
|
||||
Testing511Song,
|
||||
deepmerge(baseBatchManualScore, { difficulty: null }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", playtype: "SP", version: null },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
@@ -294,7 +278,7 @@ t.test("#ResolveChartFromSong", (t) => {
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", playtype: "SP", version: null },
|
||||
importType
|
||||
),
|
||||
new InvalidScoreFailure(
|
||||
@@ -312,7 +296,7 @@ t.test("#ResolveChartFromSong", (t) => {
|
||||
Testing511Song,
|
||||
// @ts-expect-error faulty deepmerge types
|
||||
deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol)
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", version: null, playtype: "SP" },
|
||||
importType
|
||||
),
|
||||
ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)")
|
||||
@@ -328,6 +312,7 @@ t.test("#ResolveChartFromSong", (t) => {
|
||||
{
|
||||
game: "iidx",
|
||||
service: "foo",
|
||||
playtype: "SP",
|
||||
version: "27",
|
||||
},
|
||||
importType
|
||||
@@ -345,7 +330,7 @@ t.test("#ConverterFn", (t) => {
|
||||
t.test("Should produce a DryScore", async (t) => {
|
||||
const res = await ConverterBatchManual(
|
||||
baseBatchManualScore,
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", playtype: "SP", version: null },
|
||||
importType,
|
||||
logger
|
||||
);
|
||||
@@ -364,7 +349,7 @@ t.test("#ConverterFn", (t) => {
|
||||
score: 500,
|
||||
grade: "E",
|
||||
// percent: 31.5, -- ish, FPA is hard.
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
@@ -380,7 +365,7 @@ t.test("#ConverterFn", (t) => {
|
||||
ConverterBatchManual(
|
||||
// @ts-expect-error broken deepmerge
|
||||
deepmerge(baseBatchManualScore, { score: 2000 }),
|
||||
{ game: "iidx", service: "foo", version: null },
|
||||
{ game: "iidx", service: "foo", playtype: "SP", version: null },
|
||||
importType,
|
||||
logger
|
||||
),
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ConverterBatchManual: ConverterFunction<BatchManualScore, BatchManu
|
||||
score: data.score,
|
||||
grade,
|
||||
percent,
|
||||
hitData: data.hitData ?? {},
|
||||
judgements: data.judgements ?? {},
|
||||
hitMeta: data.hitMeta ?? {},
|
||||
},
|
||||
scoreMeta: {}, // @todo #74
|
||||
@@ -95,6 +95,12 @@ export async function ResolveMatchTypeToKTData(
|
||||
);
|
||||
}
|
||||
|
||||
if (chart.playtype !== context.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Chart ${chart.chartID}'s playtype was ${chart.playtype}, but this was not equal to the import playtype of ${context.playtype}.`
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID(game, chart.songID);
|
||||
|
||||
if (!song) {
|
||||
@@ -114,19 +120,13 @@ export async function ResolveMatchTypeToKTData(
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but is needed for ddrSongHash lookup.`
|
||||
);
|
||||
}
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype);
|
||||
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype);
|
||||
|
||||
const chart = await FindDDRChartOnSongHash(data.identifier, data.playtype, difficulty);
|
||||
const chart = await FindDDRChartOnSongHash(data.identifier, context.playtype, difficulty);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`,
|
||||
`Cannot find chart for songHash ${data.identifier} (${context.playtype} ${difficulty}).`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
@@ -199,13 +199,7 @@ export async function ResolveChartFromSong(
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.playtype) {
|
||||
throw new InvalidScoreFailure(
|
||||
`Missing 'playtype' field, but was necessary for this lookup.`
|
||||
);
|
||||
}
|
||||
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype);
|
||||
const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype);
|
||||
|
||||
let chart;
|
||||
|
||||
@@ -213,17 +207,17 @@ export async function ResolveChartFromSong(
|
||||
chart = await FindChartWithPTDFVersion(
|
||||
game,
|
||||
song.id,
|
||||
data.playtype,
|
||||
context.playtype,
|
||||
difficulty,
|
||||
context.version
|
||||
);
|
||||
} else {
|
||||
chart = await FindChartWithPTDF(game, song.id, data.playtype, difficulty);
|
||||
chart = await FindChartWithPTDF(game, song.id, context.playtype, difficulty);
|
||||
}
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`,
|
||||
`Cannot find chart for ${song.title} (${context.playtype} ${difficulty})`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
|
||||
@@ -18,7 +18,7 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
const baseBatchManual = {
|
||||
body: [],
|
||||
head: { service: "foo", game: "iidx" },
|
||||
head: { service: "foo", game: "iidx", playtype: "SP" },
|
||||
};
|
||||
|
||||
const baseBatchManualScore = {
|
||||
@@ -26,7 +26,6 @@ const baseBatchManualScore = {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
};
|
||||
|
||||
@@ -68,7 +67,12 @@ t.test("#ParserFn", (t) => {
|
||||
|
||||
t.test("No Game", (t) => {
|
||||
t.throws(
|
||||
() => ParserFn({ body: [], head: { service: "foo" } }, "file/batch-manual", logger),
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: "foo", playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
new ScoreImportFatalError(
|
||||
400,
|
||||
"Could not retrieve head.game - is this valid BATCH-MANUAL?"
|
||||
@@ -79,11 +83,29 @@ t.test("#ParserFn", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("No Playtype", (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: "foo", game: "iidx" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
new ScoreImportFatalError(
|
||||
400,
|
||||
"Could not retrieve head.playtype - is this valid BATCH-MANUAL?"
|
||||
),
|
||||
"Should throw an error."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Invalid Game", (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: "foo", game: "invalid_game" } },
|
||||
{ body: [], head: { service: "foo", game: "invalid_game", playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
@@ -93,7 +115,7 @@ t.test("#ParserFn", (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: "foo", game: 123 } },
|
||||
{ body: [], head: { service: "foo", game: 123, playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
@@ -107,7 +129,7 @@ t.test("#ParserFn", (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: "1", game: "iidx" } },
|
||||
{ body: [], head: { service: "1", game: "iidx", playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
@@ -121,7 +143,7 @@ t.test("#ParserFn", (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
ParserFn(
|
||||
{ body: [], head: { service: 1, game: "iidx" } },
|
||||
{ body: [], head: { service: 1, game: "iidx", playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
),
|
||||
@@ -137,7 +159,7 @@ t.test("#ParserFn", (t) => {
|
||||
|
||||
t.test("Valid Empty BATCH-MANUAL", (t) => {
|
||||
const res = ParserFn(
|
||||
{ body: [], head: { service: "foo", game: "iidx" } },
|
||||
{ body: [], head: { service: "foo", game: "iidx", playtype: "SP" } },
|
||||
"file/batch-manual",
|
||||
logger
|
||||
);
|
||||
@@ -165,7 +187,6 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
},
|
||||
{
|
||||
@@ -173,7 +194,6 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "kamaitachiSongID",
|
||||
identifier: "123",
|
||||
playtype: "DP",
|
||||
difficulty: "HYPER",
|
||||
},
|
||||
{
|
||||
@@ -189,7 +209,7 @@ t.test("#ParserFn", (t) => {
|
||||
identifier: "5.1.1.",
|
||||
},
|
||||
],
|
||||
head: { service: "foo", game: "iidx" },
|
||||
head: { service: "foo", game: "iidx", playtype: "SP" },
|
||||
} as BatchManual,
|
||||
"file/batch-manual",
|
||||
logger
|
||||
@@ -200,6 +220,7 @@ t.test("#ParserFn", (t) => {
|
||||
context: {
|
||||
service: "foo",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
version: null,
|
||||
},
|
||||
iterable: [
|
||||
@@ -208,7 +229,6 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
},
|
||||
{
|
||||
@@ -216,7 +236,6 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "kamaitachiSongID",
|
||||
identifier: "123",
|
||||
playtype: "DP",
|
||||
difficulty: "HYPER",
|
||||
},
|
||||
{
|
||||
@@ -249,6 +268,7 @@ t.test("#ParserFn", (t) => {
|
||||
context: {
|
||||
service: "foo",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
version: null,
|
||||
},
|
||||
iterable: [
|
||||
@@ -257,7 +277,6 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
hitMeta: {
|
||||
bp: 10,
|
||||
@@ -272,9 +291,9 @@ t.test("#ParserFn", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Valid HitData", (t) => {
|
||||
t.test("Valid judgements", (t) => {
|
||||
const res = ParserFn(
|
||||
dm({ hitData: { pgreat: 1, great: null, bad: 0 } }),
|
||||
dm({ judgements: { pgreat: 1, great: null, bad: 0 } }),
|
||||
"file/batch-manual",
|
||||
logger
|
||||
);
|
||||
@@ -284,6 +303,7 @@ t.test("#ParserFn", (t) => {
|
||||
context: {
|
||||
service: "foo",
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
version: null,
|
||||
},
|
||||
iterable: [
|
||||
@@ -292,9 +312,8 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
hitData: {
|
||||
judgements: {
|
||||
pgreat: 1,
|
||||
great: null,
|
||||
bad: 0,
|
||||
@@ -320,11 +339,10 @@ t.test("#ParserFn", (t) => {
|
||||
lamp: "ALL JUSTICE", // not an iidx lamp
|
||||
matchType: "songID",
|
||||
identifier: "123",
|
||||
playtype: "SP",
|
||||
difficulty: "ANOTHER",
|
||||
},
|
||||
],
|
||||
head: { service: "foo", game: "iidx" },
|
||||
head: { service: "foo", game: "iidx", playtype: "SP" },
|
||||
},
|
||||
"file/batch-manual",
|
||||
logger
|
||||
@@ -385,21 +403,6 @@ t.test("#ParserFn", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Invalid Playtype", (t) => {
|
||||
// this is not a valid playtype for IIDX
|
||||
const fn = () => ParserFn(dm({ playtype: "Single" }), "file/batch-manual", logger);
|
||||
|
||||
t.throws(
|
||||
fn,
|
||||
new ScoreImportFatalError(
|
||||
400,
|
||||
"Invalid BATCH-MANUAL: body[0].playtype | Expected any of SP, DP. | Received Single [string]."
|
||||
)
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Invalid Identifier", (t) => {
|
||||
// this is not a valid playtype for IIDX
|
||||
const fn = () => ParserFn(dm({ identifier: null }), "file/batch-manual", logger);
|
||||
@@ -424,18 +427,18 @@ t.test("#ParserFn", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Invalid HitData", (t) => {
|
||||
t.test("Invalid judgements", (t) => {
|
||||
const fn = () =>
|
||||
ParserFn(dm({ hitData: { not_key: 123 } }), "file/batch-manual", logger);
|
||||
ParserFn(dm({ judgements: { not_key: 123 } }), "file/batch-manual", logger);
|
||||
|
||||
t.throws(fn, mockErr("body[0].hitData | Invalid Key not_key"));
|
||||
t.throws(fn, mockErr("body[0].judgements | Invalid Key not_key"));
|
||||
|
||||
const fn2 = () =>
|
||||
ParserFn(dm({ hitData: { pgreat: "123" } }), "file/batch-manual", logger);
|
||||
ParserFn(dm({ judgements: { pgreat: "123" } }), "file/batch-manual", logger);
|
||||
|
||||
t.throws(
|
||||
fn2,
|
||||
mockErr("body[0].hitData | Key pgreat had an invalid value of 123 [string]")
|
||||
mockErr("body[0].judgements | Key pgreat had an invalid value of 123 [string]")
|
||||
);
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -2,8 +2,7 @@ import { KtLogger } from "../../../../logger/logger";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { BatchManual, BatchManualContext, BatchManualScore } from "./types";
|
||||
import p, { PrudenceSchema, ValidSchemaValue } from "prudence";
|
||||
import { lamps, validHitData, validPlaytypes } from "tachi-common/js/config";
|
||||
import { Game, ImportTypes } from "tachi-common";
|
||||
import { Game, ImportTypes, GetGamePTConfig, Playtypes, GetGameConfig } from "tachi-common";
|
||||
import deepmerge from "deepmerge";
|
||||
import { FormatPrError } from "../../../../../utils/prudence";
|
||||
import { ParserFunctionReturns } from "../types";
|
||||
@@ -25,7 +24,7 @@ const PR_HitMeta = (game: Game): PrudenceSchema => {
|
||||
gaugeHistory: optNull([p.isBoundedInteger(0, 100)]),
|
||||
comboBreak: optNull(p.isPositiveInteger),
|
||||
};
|
||||
} else if (game === "popn" || game === "sdvx" || game === "usc") {
|
||||
} else if (/* game === "popn" || */ game === "sdvx" || game === "usc") {
|
||||
return {
|
||||
gauge: optNull(p.isBoundedInteger(0, 100)),
|
||||
};
|
||||
@@ -50,61 +49,66 @@ const PR_HitMeta = (game: Game): PrudenceSchema => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const PR_BatchManualScore = (game: Game): PrudenceSchema => ({
|
||||
score: "number",
|
||||
lamp: p.isIn(lamps[game]),
|
||||
matchType: p.isIn(
|
||||
"songTitle",
|
||||
"ddrSongHash",
|
||||
"kamaitachiSongID",
|
||||
"bmsChartHash",
|
||||
"title",
|
||||
"songHash",
|
||||
"songID",
|
||||
"hash"
|
||||
),
|
||||
identifier: "string",
|
||||
comment: optNull(p.isBoundedString(3, 240)),
|
||||
playtype: optNull(p.isIn(validPlaytypes[game])),
|
||||
difficulty: "*?string", // this is checked in converting instead
|
||||
// september 9th 2001 - this saves people not
|
||||
// reading the documentation.
|
||||
timeAchieved: optNull(
|
||||
(self) =>
|
||||
(typeof self === "number" && self > 1_000_000_000_000) ||
|
||||
"Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds?"
|
||||
),
|
||||
hitData: optNull((self) => {
|
||||
if (typeof self !== "object" || self === null) {
|
||||
return "Not a valid object.";
|
||||
}
|
||||
|
||||
for (const key in self) {
|
||||
if (!validHitData[game].includes(key)) {
|
||||
return `Invalid Key ${key}. Expected any of ${validHitData[game].join(", ")}`;
|
||||
const PR_BatchManualScore = (game: Game, playtype: Playtypes[Game]): PrudenceSchema => {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
return {
|
||||
score: "number",
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
matchType: p.isIn(
|
||||
"songTitle",
|
||||
"ddrSongHash",
|
||||
"kamaitachiSongID",
|
||||
"bmsChartHash",
|
||||
"title",
|
||||
"songHash",
|
||||
"songID",
|
||||
"hash"
|
||||
),
|
||||
identifier: "string",
|
||||
comment: optNull(p.isBoundedString(3, 240)),
|
||||
difficulty: "*?string", // this is checked in converting instead
|
||||
// september 9th 2001 - this saves people not
|
||||
// reading the documentation.
|
||||
timeAchieved: optNull(
|
||||
(self) =>
|
||||
(typeof self === "number" && self > 1_000_000_000_000) ||
|
||||
"Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds?"
|
||||
),
|
||||
judgements: optNull((self) => {
|
||||
if (typeof self !== "object" || self === null) {
|
||||
return "Not a valid object.";
|
||||
}
|
||||
|
||||
// @ts-expect-error shush
|
||||
const v = self[key];
|
||||
if ((!Number.isSafeInteger(v) || v < 0) && v !== null) {
|
||||
return `Key ${key} had an invalid value of ${v} [${typeof v}]`;
|
||||
for (const key in self) {
|
||||
if (!gptConfig.judgements.includes(key)) {
|
||||
return `Invalid Key ${key}. Expected any of ${gptConfig.judgements.join(", ")}`;
|
||||
}
|
||||
|
||||
// @ts-expect-error shush
|
||||
const v = self[key];
|
||||
if ((!Number.isSafeInteger(v) || v < 0) && v !== null) {
|
||||
return `Key ${key} had an invalid value of ${v} [${typeof v}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
hitMeta: optNull(deepmerge(BaseValidHitMeta, PR_HitMeta(game)) as unknown as ValidSchemaValue),
|
||||
// scoreMeta: @todo #74
|
||||
// more game specific props, maybe?
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
hitMeta: optNull(
|
||||
deepmerge(BaseValidHitMeta, PR_HitMeta(game)) as unknown as ValidSchemaValue
|
||||
),
|
||||
// scoreMeta: @todo #74
|
||||
// more game specific props, maybe?
|
||||
};
|
||||
};
|
||||
|
||||
const PR_BatchManual = (game: Game): PrudenceSchema => ({
|
||||
const PR_BatchManual = (game: Game, playtype: Playtypes[Game]): PrudenceSchema => ({
|
||||
head: {
|
||||
service: p.isBoundedString(3, 15),
|
||||
game: p.isIn(CONF_INFO.SUPPORTED_GAMES),
|
||||
game: p.isIn(CONF_INFO.supportedGames),
|
||||
playtype: p.is(playtype),
|
||||
version: "*?string",
|
||||
},
|
||||
body: [PR_BatchManualScore(game)],
|
||||
body: [PR_BatchManualScore(game, playtype)],
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -132,6 +136,8 @@ export function ParseBatchManualFromObject(
|
||||
// attempt to retrieve game
|
||||
// @ts-expect-error man.
|
||||
const possiblyGame = object?.head?.game;
|
||||
// @ts-expect-error man.
|
||||
const possiblyPlaytype = object?.head?.playtype;
|
||||
|
||||
if (!possiblyGame) {
|
||||
throw new ScoreImportFatalError(
|
||||
@@ -140,21 +146,41 @@ export function ParseBatchManualFromObject(
|
||||
);
|
||||
}
|
||||
|
||||
if (!CONF_INFO.SUPPORTED_GAMES.includes(possiblyGame)) {
|
||||
if (!possiblyPlaytype) {
|
||||
throw new ScoreImportFatalError(
|
||||
400,
|
||||
`Invalid game ${possiblyGame} - expected any of ${CONF_INFO.SUPPORTED_GAMES.join(", ")}`
|
||||
`Could not retrieve head.playtype - is this valid BATCH-MANUAL?`
|
||||
);
|
||||
}
|
||||
|
||||
if (!CONF_INFO.supportedGames.includes(possiblyGame)) {
|
||||
throw new ScoreImportFatalError(
|
||||
400,
|
||||
`Invalid game ${possiblyGame} - expected any of ${CONF_INFO.supportedGames.join(", ")}.`
|
||||
);
|
||||
}
|
||||
|
||||
const game: Game = possiblyGame;
|
||||
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
if (!gameConfig.validPlaytypes.includes(possiblyPlaytype)) {
|
||||
throw new ScoreImportFatalError(
|
||||
400,
|
||||
`Invalid playtype ${possiblyPlaytype} - expected any of ${gameConfig.validPlaytypes.join(
|
||||
", "
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
const playtype: Playtypes[Game] = possiblyPlaytype;
|
||||
|
||||
// now that we have the game, we can validate this against
|
||||
// the prudence schema for batch-manual.
|
||||
// This mostly works as a sanity check, and doesn't
|
||||
// check things like whether a score is > 100%
|
||||
// or something.
|
||||
const err = p(object, PR_BatchManual(game));
|
||||
const err = p(object, PR_BatchManual(game, playtype));
|
||||
|
||||
if (err) {
|
||||
throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid BATCH-MANUAL"));
|
||||
@@ -167,6 +193,7 @@ export function ParseBatchManualFromObject(
|
||||
context: {
|
||||
service: batchManual.head.service,
|
||||
game,
|
||||
playtype,
|
||||
version: batchManual.head.version ?? null,
|
||||
},
|
||||
iterable: batchManual.body,
|
||||
|
||||
@@ -5,7 +5,7 @@ export type BatchManualScore = {
|
||||
lamp: Lamps[IDStrings];
|
||||
timeAchieved?: number | null;
|
||||
comment?: string | null;
|
||||
hitData?: Record<string, unknown> | null;
|
||||
judgements?: Record<string, unknown> | null;
|
||||
hitMeta?: Record<string, unknown> | null;
|
||||
identifier: string;
|
||||
matchType:
|
||||
@@ -18,7 +18,6 @@ export type BatchManualScore = {
|
||||
| "songID" // songID is legacy - use kamaitachiSongID
|
||||
| "hash"; // hash is legacy - use bmsChartHash
|
||||
|
||||
playtype?: Playtypes[Game] | null;
|
||||
difficulty?: string | null; // lazy...
|
||||
};
|
||||
|
||||
@@ -33,6 +32,7 @@ export interface BatchManual {
|
||||
|
||||
export interface BatchManualContext {
|
||||
game: Game;
|
||||
playtype: Playtypes[Game];
|
||||
service: string;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ const ConvertEamIIDXCSV: ConverterFunction<IIDXEamusementCSVData, IIDXEamusement
|
||||
scoreData: {
|
||||
score: exscore,
|
||||
lamp,
|
||||
hitData: {
|
||||
judgements: {
|
||||
pgreat,
|
||||
great,
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ t.test("#ConvertFileMerIIDX", (t) => {
|
||||
// percent: 63.61, approximately, fpa impossible.
|
||||
grade: "B",
|
||||
lamp: "CLEAR",
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: 21,
|
||||
},
|
||||
@@ -76,7 +76,7 @@ t.test("#ConvertFileMerIIDX", (t) => {
|
||||
// percent: 63.61, approximately, fpa impossible.
|
||||
grade: "B",
|
||||
lamp: "CLEAR",
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: 21,
|
||||
},
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ConvertFileMerIIDX: ConverterFunction<MerScore, EmptyObject> = asyn
|
||||
percent,
|
||||
grade,
|
||||
lamp,
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: data.miss_count === -1 ? null : data.miss_count,
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ t.test("#ConvertFileS3", (t) => {
|
||||
grade: "F",
|
||||
score: 100,
|
||||
lamp: "FULL COMBO",
|
||||
hitData: {
|
||||
judgements: {
|
||||
pgreat: 25,
|
||||
great: 50,
|
||||
good: 0,
|
||||
|
||||
@@ -142,9 +142,9 @@ export const ConvertFileS3: ConverterFunction<S3Score, EmptyObject> = async (
|
||||
|
||||
const timeAchieved = ParseDateFromString(data.date);
|
||||
|
||||
let hitData = {};
|
||||
let judgements = {};
|
||||
if (data.scorebreakdown) {
|
||||
hitData = {
|
||||
judgements = {
|
||||
pgreat: data.scorebreakdown.justgreats,
|
||||
great: data.scorebreakdown.greats,
|
||||
good: data.scorebreakdown.good,
|
||||
@@ -163,7 +163,7 @@ export const ConvertFileS3: ConverterFunction<S3Score, EmptyObject> = async (
|
||||
grade: grade as Grades["iidx:SP" | "iidx:DP"],
|
||||
score: data.exscore,
|
||||
lamp,
|
||||
hitData,
|
||||
judgements,
|
||||
hitMeta: {},
|
||||
},
|
||||
scoreMeta: {},
|
||||
|
||||
@@ -61,7 +61,7 @@ t.test("#ConverterIRBarbatos", (t) => {
|
||||
percent: 90,
|
||||
grade: "A+",
|
||||
lamp: "CLEAR",
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: 100,
|
||||
near: 50,
|
||||
miss: 5,
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ConverterIRBarbatos: ConverterFunction<BarbatosScore, EmptyObject>
|
||||
percent,
|
||||
grade,
|
||||
lamp: LAMP_LOOKUP[data.clear_type] as Lamps["sdvx:Single"],
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: data.critical,
|
||||
near: data.near_total,
|
||||
miss: data.error,
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
|
||||
gauge: data.gauge === -1 ? null : data.gauge,
|
||||
};
|
||||
|
||||
let hitData: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitData"] = {};
|
||||
let judgements: DryScore<"bms:7K" | "bms:14K">["scoreData"]["judgements"] = {};
|
||||
|
||||
// if NOT CN mode we can assign judgements properly
|
||||
if (data.lntype === 0) {
|
||||
@@ -95,7 +95,7 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
|
||||
hitMeta.epr! += data.ems;
|
||||
hitMeta.lpr! += data.lms;
|
||||
|
||||
hitData = {
|
||||
judgements = {
|
||||
pgreat: data.epg + data.lpg,
|
||||
great: data.egr + data.lgr,
|
||||
good: data.egd + data.lgd,
|
||||
@@ -122,7 +122,7 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
|
||||
score: data.exscore,
|
||||
lamp,
|
||||
hitMeta,
|
||||
hitData,
|
||||
judgements,
|
||||
},
|
||||
scoreMeta: {
|
||||
client: context.client,
|
||||
|
||||
@@ -54,7 +54,7 @@ export const ConverterIRFervidexStatic: ConverterFunction<
|
||||
percent,
|
||||
grade,
|
||||
lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"],
|
||||
hitData: {},
|
||||
judgements: {},
|
||||
hitMeta: {
|
||||
bp: data.miss_count === -1 ? null : data.miss_count,
|
||||
},
|
||||
|
||||
@@ -94,7 +94,7 @@ const baseDryScore = {
|
||||
percent: 4.325699745547074,
|
||||
grade: "F",
|
||||
lamp: "FAILED",
|
||||
hitData: {
|
||||
judgements: {
|
||||
pgreat: 34,
|
||||
great: 0,
|
||||
good: 0,
|
||||
|
||||
@@ -189,7 +189,7 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
|
||||
percent,
|
||||
grade,
|
||||
lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"],
|
||||
hitData: {
|
||||
judgements: {
|
||||
pgreat: data.pgreat,
|
||||
great: data.great,
|
||||
good: data.good,
|
||||
|
||||
@@ -91,7 +91,7 @@ export const ConverterIRUSC: ConverterFunction<USCClientScore, IRUSCContext> = a
|
||||
percent,
|
||||
score: data.score,
|
||||
lamp: DeriveLamp(data, logger),
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: data.crit,
|
||||
near: data.near,
|
||||
miss: data.error,
|
||||
|
||||
@@ -4,7 +4,7 @@ import JSON5 from "json5";
|
||||
import fs from "fs";
|
||||
import p from "prudence";
|
||||
import { FormatPrError } from "../../utils/prudence";
|
||||
import { integer, Game } from "tachi-common";
|
||||
import { integer, Game, StaticConfig } from "tachi-common";
|
||||
dotenv.config(); // imports things like NODE_ENV from a local .env file if one is present.
|
||||
|
||||
// reads from $pwd/conf.json5
|
||||
@@ -38,10 +38,7 @@ export interface TachiConfig {
|
||||
CDN_ROOT: string;
|
||||
TYPE: "ktchi" | "btchi" | "omni";
|
||||
PORT: integer;
|
||||
TYPE_INFO: {
|
||||
NAME: string;
|
||||
SUPPORTED_GAMES: Game[];
|
||||
};
|
||||
TYPE_INFO: StaticConfig.ServerConfig;
|
||||
}
|
||||
|
||||
const err = p(config, {
|
||||
@@ -63,39 +60,12 @@ if (err) {
|
||||
throw FormatPrError(err, "Invalid conf.json5 file.");
|
||||
}
|
||||
|
||||
/**
|
||||
* KTCHI | Kamaitachi is the arcade build of Tachi.
|
||||
*/
|
||||
const KTCHI_INFO: TachiConfig["TYPE_INFO"] = {
|
||||
NAME: "Kamaitachi",
|
||||
SUPPORTED_GAMES: ["iidx", "gitadora", "chunithm", "maimai", "museca", "sdvx"],
|
||||
};
|
||||
|
||||
/**
|
||||
* BTCHI | Bokutachi is the home-simulator build of Tachi.
|
||||
*/
|
||||
const BTCHI_INFO: TachiConfig["TYPE_INFO"] = {
|
||||
NAME: "Bokutachi",
|
||||
SUPPORTED_GAMES: ["usc", "bms"],
|
||||
};
|
||||
|
||||
/**
|
||||
* OMNI | Omnitachi is a local development/testing build of Tachi, which enables all endpoints.
|
||||
*
|
||||
* If you're one of those nerds who is reading this to figure out how to steal and rerun
|
||||
* this codebase this is what you're looking for.
|
||||
*/
|
||||
const OMNI_INFO: TachiConfig["TYPE_INFO"] = {
|
||||
NAME: "Omnitachi",
|
||||
SUPPORTED_GAMES: [...KTCHI_INFO.SUPPORTED_GAMES, ...BTCHI_INFO.SUPPORTED_GAMES],
|
||||
};
|
||||
|
||||
if (config.TYPE === "ktchi") {
|
||||
config.TYPE_INFO = KTCHI_INFO;
|
||||
config.TYPE_INFO = StaticConfig.KTCHI_CONFIG;
|
||||
} else if (config.TYPE === "btchi") {
|
||||
config.TYPE_INFO = BTCHI_INFO;
|
||||
config.TYPE_INFO = StaticConfig.BTCHI_CONFIG;
|
||||
} else if (config.TYPE === "omni") {
|
||||
config.TYPE_INFO = OMNI_INFO;
|
||||
config.TYPE_INFO = StaticConfig.OMNI_CONFIG;
|
||||
}
|
||||
|
||||
const tachiConfig = config as TachiConfig;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { FormatVersion } from "./lib/constants/version";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
logger.info(`Booting ${CONF_INFO.NAME} - ${FormatVersion()} [ENV: ${process.env.NODE_ENV}]`);
|
||||
logger.info(`Booting ${CONF_INFO.name} - ${FormatVersion()} [ENV: ${process.env.NODE_ENV}]`);
|
||||
logger.info(`Log level is set to ${LOG_LEVEL}.`);
|
||||
|
||||
server.listen(PORT);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Router } from "express";
|
||||
import { FileUploadImportTypes } from "tachi-common";
|
||||
import { fileImportTypes } from "tachi-common/js/config";
|
||||
import Prudence from "prudence";
|
||||
import { GetUserWithIDGuaranteed } from "../../../../../utils/user";
|
||||
import CreateLogCtx, { KtLogger } from "../../../../../lib/logger/logger";
|
||||
@@ -16,7 +15,7 @@ import ParseBatchManual from "../../../../../lib/score-import/import-types/file/
|
||||
import { ParseSolidStateXML } from "../../../../../lib/score-import/import-types/file/solid-state-squad/parser";
|
||||
import { ParseMerIIDX } from "../../../../../lib/score-import/import-types/file/mer-iidx/parser";
|
||||
import ParsePLIIIDXCSV from "../../../../../lib/score-import/import-types/file/pli-iidx-csv/parser";
|
||||
import { RequireKamaitachi } from "../../../../middleware/type-require";
|
||||
import { CONF_INFO } from "../../../../../lib/setup/config";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -35,11 +34,10 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware(
|
||||
router.post(
|
||||
"/file",
|
||||
RequireLoggedInSession,
|
||||
RequireKamaitachi, // This is only useful for Kamaitachi, so.
|
||||
ParseMultipartScoredata,
|
||||
prValidate(
|
||||
{
|
||||
importType: Prudence.isIn(fileImportTypes),
|
||||
importType: Prudence.isIn(CONF_INFO.supportedImportTypes),
|
||||
},
|
||||
{},
|
||||
{ allowExcessKeys: true }
|
||||
|
||||
@@ -2,20 +2,15 @@ import { Router } from "express";
|
||||
import db from "../../../../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "../../../../../../../../../lib/constants/tachi";
|
||||
import { GetRelevantSongsAndCharts } from "../../../../../../../../../utils/db";
|
||||
import {
|
||||
GetDefaultScoreRatingAlg,
|
||||
GetDefaultSessionRatingAlg,
|
||||
GetUsersRanking,
|
||||
} from "../../../../../../../../../utils/user";
|
||||
import { GetUsersRanking } from "../../../../../../../../../utils/user";
|
||||
import { CheckUserPlayedGamePlaytype } from "./middleware";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import { UserGoalDocument, UserMilestoneDocument } from "tachi-common";
|
||||
import { UserGoalDocument, UserMilestoneDocument, GetGamePTConfig } from "tachi-common";
|
||||
import {
|
||||
SearchGameSongsAndCharts,
|
||||
SearchSessions,
|
||||
} from "../../../../../../../../../lib/search/search";
|
||||
import { FilterChartsAndSongs } from "../../../../../../../../../utils/scores";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use(CheckUserPlayedGamePlaytype);
|
||||
@@ -298,6 +293,7 @@ router.get("/pbs/best", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const pbs = await db["personal-bests"].find(
|
||||
{
|
||||
@@ -309,7 +305,7 @@ router.get("/pbs/best", async (req, res) => {
|
||||
{
|
||||
limit: 100,
|
||||
sort: {
|
||||
[`calculatedData.${GetDefaultScoreRatingAlg(game, playtype)}`]: -1,
|
||||
[`calculatedData.${gptConfig.defaultScoreRatingAlg}`]: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -365,6 +361,7 @@ router.get("/sessions/best", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const sessions = await db.sessions.find(
|
||||
{
|
||||
@@ -375,7 +372,7 @@ router.get("/sessions/best", async (req, res) => {
|
||||
{
|
||||
limit: 100,
|
||||
sort: {
|
||||
[`calculatedData.${GetDefaultSessionRatingAlg(game, playtype)}`]: -1,
|
||||
[`calculatedData.${gptConfig.defaultSessionRatingAlg}`]: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,8 +8,6 @@ import { GetKTDataJSON } from "../../../../test-utils/test-data";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function TestHeaders(url: string, data: any) {
|
||||
t.beforeEach(InsertFakeTokenWithAllPerms("mock_token"));
|
||||
|
||||
t.test("Should reject invalid X-Software-Models", async (t) => {
|
||||
let res = await mockApi
|
||||
.post(url)
|
||||
|
||||
@@ -124,7 +124,7 @@ const USC_SCORE_PB: PBScoreDocument = {
|
||||
lamp: "EXCESSIVE CLEAR",
|
||||
lampIndex: 2,
|
||||
gradeIndex: 4, // idk, lazy
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: 50,
|
||||
near: 30,
|
||||
miss: 10,
|
||||
|
||||
@@ -28,7 +28,7 @@ const mockScorePB: PBScoreDocument<"usc:Single"> = {
|
||||
esd: null,
|
||||
grade: "AAA+",
|
||||
gradeIndex: 7,
|
||||
hitData: {
|
||||
judgements: {
|
||||
critical: 100,
|
||||
miss: 15,
|
||||
},
|
||||
|
||||
@@ -69,9 +69,9 @@ export async function TachiScoreToServerScore(
|
||||
return {
|
||||
score: tachiScore.scoreData.score,
|
||||
timestamp: MStoS(tachiScore.timeAchieved ?? 0),
|
||||
crit: tachiScore.scoreData.hitData.critical ?? 0,
|
||||
near: tachiScore.scoreData.hitData.near ?? 0,
|
||||
error: tachiScore.scoreData.hitData.miss ?? 0,
|
||||
crit: tachiScore.scoreData.judgements.critical ?? 0,
|
||||
near: tachiScore.scoreData.judgements.near ?? 0,
|
||||
error: tachiScore.scoreData.judgements.miss ?? 0,
|
||||
ranking: tachiScore.rankingData.rank,
|
||||
lamp: TACHI_LAMP_TO_USC[tachiScore.scoreData.lamp],
|
||||
username: userDoc.username,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"esd": 30,
|
||||
"gradeIndex": 3,
|
||||
"lampIndex": 4,
|
||||
"hitData": {
|
||||
"judgements": {
|
||||
"pgreat": 50,
|
||||
"great": 10
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"head": {
|
||||
"game": "chunithm",
|
||||
"playtype": "Single",
|
||||
"service": "Chunitachi"
|
||||
},
|
||||
"body": [{
|
||||
@@ -8,7 +9,6 @@
|
||||
"lamp": "CLEAR",
|
||||
"matchType": "songTitle",
|
||||
"identifier": "B.B.K.K.B.K.K.",
|
||||
"playtype": "Single",
|
||||
"difficulty": "BASIC"
|
||||
}]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"head": {
|
||||
"game": "iidx",
|
||||
"playtype": "SP",
|
||||
"service": "foobar"
|
||||
},
|
||||
"body": []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"head": {
|
||||
"game": "iidx",
|
||||
"playtype": "SP",
|
||||
"service": "foobar"
|
||||
},
|
||||
"body": [{
|
||||
@@ -8,7 +9,6 @@
|
||||
"lamp": "HARD CLEAR",
|
||||
"matchType": "songTitle",
|
||||
"identifier": "5.1.1.",
|
||||
"playtype": "SP",
|
||||
"difficulty": "ANOTHER"
|
||||
}]
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"percent": 70.76,
|
||||
"grade": "B",
|
||||
"esd": 30,
|
||||
"hitData": {},
|
||||
"judgements": {},
|
||||
"hitMeta": {}
|
||||
},
|
||||
"scoreMeta": {},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"lamp": "CLEAR",
|
||||
"percent": 50,
|
||||
"grade": "C",
|
||||
"hitData": {
|
||||
"judgements": {
|
||||
"pgreat": 50,
|
||||
"great": 10
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"esd": 30,
|
||||
"gradeIndex": 3,
|
||||
"lampIndex": 4,
|
||||
"hitData": {
|
||||
"judgements": {
|
||||
"pgreat": 50,
|
||||
"great": 10
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"gradeIndex": -1,
|
||||
"lamp": "EX HARD CLEAR",
|
||||
"lampIndex": 6,
|
||||
"hitData": {
|
||||
"judgements": {
|
||||
"pgreat": 697,
|
||||
"great": 85
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"percent": 95,
|
||||
"grade": "AA+",
|
||||
"esd": null,
|
||||
"hitData": {},
|
||||
"judgements": {},
|
||||
"hitMeta": {}
|
||||
},
|
||||
"scoreMeta": {},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import { CONF_INFO } from "../lib/setup/config";
|
||||
import { Game, Playtypes } from "tachi-common";
|
||||
import { validPlaytypes } from "tachi-common/js/config";
|
||||
import { Game, Playtypes, GetGameConfig } from "tachi-common";
|
||||
|
||||
// https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
|
||||
// the developer of this has migrated everything to Force ES6 style modules,
|
||||
@@ -63,9 +62,9 @@ export function HasOwnProperty<T>(obj: T, key: string | number | symbol): key is
|
||||
}
|
||||
|
||||
export function IsValidGame(str: string): str is Game {
|
||||
return !!CONF_INFO.SUPPORTED_GAMES.includes(str as Game);
|
||||
return !!CONF_INFO.supportedGames.includes(str as Game);
|
||||
}
|
||||
|
||||
export function IsValidPlaytype(game: Game, str: string): str is Playtypes[Game] {
|
||||
return validPlaytypes[game].includes(str as Playtypes[Game]);
|
||||
return GetGameConfig(game).validPlaytypes.includes(str as Playtypes[Game]);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { integer, PublicUserDocument, UserGameStats, Game, Playtypes } from "tachi-common";
|
||||
import { integer, PublicUserDocument, UserGameStats, GetGamePTConfig } from "tachi-common";
|
||||
import { FindOneResult } from "monk";
|
||||
import db from "../external/mongo/db";
|
||||
import CreateLogCtx from "../lib/logger/logger";
|
||||
import {
|
||||
defaultRatingAlgorithm,
|
||||
defaultScoreRatingAlgorithm,
|
||||
defaultSessionRatingAlgorithm,
|
||||
} from "tachi-common/js/config";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const OMIT_PRIVATE_USER_RETURNS = {
|
||||
@@ -144,25 +138,8 @@ export function FormatUserDoc(userdoc: PublicUserDocument) {
|
||||
return `${userdoc.username} (#${userdoc.id})`;
|
||||
}
|
||||
|
||||
// temp function, to be removed with #186
|
||||
export function GetDefaultProfileRatingAlg(game: Game, playtype: Playtypes[Game]) {
|
||||
// @ts-expect-error garbage...
|
||||
return defaultRatingAlgorithm[game][playtype];
|
||||
}
|
||||
|
||||
// temp function, to be removed with #186
|
||||
export function GetDefaultScoreRatingAlg(game: Game, playtype: Playtypes[Game]) {
|
||||
// @ts-expect-error garbage...
|
||||
return defaultScoreRatingAlgorithm[game][playtype];
|
||||
}
|
||||
|
||||
export function GetDefaultSessionRatingAlg(game: Game, playtype: Playtypes[Game]) {
|
||||
// @ts-expect-error garbage...
|
||||
return defaultSessionRatingAlgorithm[game][playtype];
|
||||
}
|
||||
|
||||
export async function GetUsersRanking(stats: UserGameStats) {
|
||||
const ratingKey = GetDefaultProfileRatingAlg(stats.game, stats.playtype);
|
||||
const gptConfig = GetGamePTConfig(stats.game, stats.playtype);
|
||||
|
||||
const aggRes = await db["game-stats"].aggregate([
|
||||
{
|
||||
@@ -179,8 +156,10 @@ export async function GetUsersRanking(stats: UserGameStats) {
|
||||
$sum: {
|
||||
$cond: {
|
||||
if: {
|
||||
// @ts-expect-error garbage...
|
||||
$gte: [`$ratings.${ratingKey}`, stats.ratings[ratingKey]],
|
||||
$gte: [
|
||||
`$ratings.${gptConfig.defaultProfileRatingAlg}`,
|
||||
stats.ratings[gptConfig.defaultProfileRatingAlg],
|
||||
],
|
||||
},
|
||||
then: 1,
|
||||
else: 0,
|
||||
|
||||
Reference in New Issue
Block a user