Merge pull request #190 from zkldi/feature/config-refactor

Config Refactor
This commit is contained in:
zkldi
2021-06-21 01:10:52 +01:00
committed by GitHub
69 changed files with 788 additions and 988 deletions
-1
View File
@@ -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",
+9 -231
View File
File diff suppressed because it is too large Load Diff
+107 -107
View File
@@ -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
View File
@@ -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();
+1 -1
View File
@@ -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),
+54 -48
View File
@@ -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",
+11 -6
View File
@@ -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();
});
+16 -5
View File
@@ -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,
+5 -35
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -28,7 +28,7 @@ const mockScorePB: PBScoreDocument<"usc:Single"> = {
esd: null,
grade: "AAA+",
gradeIndex: 7,
hitData: {
judgements: {
critical: 100,
miss: 15,
},
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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": {},
+3 -4
View File
@@ -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]);
}
+6 -27
View File
@@ -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,